// Copyright 2025 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "dawn/replay/Replay.h"

#include <webgpu/webgpu_cpp.h>

#include <algorithm>
#include <iostream>
#include <memory>
#include <ranges>
#include <set>
#include <span>
#include <string>
#include <utility>
#include <variant>
#include <vector>

#include "absl/container/flat_hash_map.h"
#include "src/dawn/common/Constants.h"
#include "src/dawn/replay/BlitBufferToDepthTexture.h"
#include "src/dawn/replay/Capture.h"
#include "src/dawn/replay/Deserialization.h"
#include "src/dawn/replay/ReplayImpl.h"
#include "src/dawn/replay/SurfaceDiscovery.h"
#include "src/utils/platform.h"

namespace dawn::replay {

namespace schema {
#define DAWN_REPLAY_ENUM_TO_STRING_MEMBER(NAME, ...) \
    case EnumType::NAME:                             \
        return #NAME;

#define DAWN_REPLAY_ENUM_TO_STRING(NAME, MEMBERS)                    \
    const char* EnumToString(schema::NAME value) {                   \
        using EnumType = NAME;                                       \
        switch (value) {                                             \
            MEMBERS(DAWN_REPLAY_ENUM_TO_STRING_MEMBER)               \
            default:                                                 \
                return "UNKNOWN";                                    \
        }                                                            \
    }                                                                \
    std::ostream& operator<<(std::ostream& os, schema::NAME value) { \
        return os << EnumToString(value);                            \
    }

DAWN_REPLAY_OBJECT_TYPES_ENUM(DAWN_REPLAY_ENUM_TO_STRING)
DAWN_REPLAY_COMMAND_BUFFER_COMMANDS_ENUM(DAWN_REPLAY_ENUM_TO_STRING)
DAWN_REPLAY_ROOT_COMMANDS_ENUM(DAWN_REPLAY_ENUM_TO_STRING)

#undef DAWN_REPLAY_ENUM_TO_STRING_MEMBER
#undef DAWN_REPLAY_ENUM_TO_STRING

}  // namespace schema

Replay::~Replay() = default;

std::unique_ptr<Replay> Replay::Create(wgpu::Device device, std::unique_ptr<Capture> capture) {
    return ReplayImpl::Create(device, std::move(capture));
}

template <typename T>
T Replay::GetObjectByLabel(std::string_view label) const {
    if (auto* impl = static_cast<const ReplayImpl*>(this)) {
        return impl->GetObjectByLabel<T>(label);
    }
    return nullptr;
}

bool Replay::Play() {
    if (auto* impl = static_cast<ReplayImpl*>(this)) {
        auto result = impl->Play();
        if (result.IsError()) {
            result.AcquireError();
            return false;
        }
        return true;
    }
    return false;
}

ReplayStatus Replay::PlayFrame() {
    if (auto* impl = static_cast<ReplayImpl*>(this)) {
        auto result = impl->PlayFrame();
        if (result.IsError()) {
            result.AcquireError();
            return ReplayStatus::Error;
        }
        return result.AcquireSuccess();
    }
    return ReplayStatus::Error;
}

#define DAWN_REPLAY_GET_OBJECT_BY_LABEL(NAME) \
    template wgpu::NAME Replay::GetObjectByLabel<wgpu::NAME>(std::string_view label) const;
DAWN_REPLAY_OBJECT_TYPES(DAWN_REPLAY_GET_OBJECT_BY_LABEL)
#undef DAWN_REPLAY_GET_OBJECT_BY_LABEL

#define DAWN_REPLAY_RESOURCE_VARIANT(NAME) wgpu::NAME,
typedef std::variant<DAWN_REPLAY_OBJECT_TYPES(DAWN_REPLAY_RESOURCE_VARIANT) std::monostate>
    Resource;
#undef DAWN_REPLAY_RESOURCE_VARIANT

struct LabeledResource {
    std::string label;
    Resource resource;
};

class DawnResourceVisitor : public ResourceVisitor {
  public:
    using ResourceVisitor::operator();
    explicit DawnResourceVisitor(DawnRootCommandVisitor* visitor) : mVisitor(visitor) {}

#define DAWN_REPLAY_RESOURCE_VISITOR_OVERRIDE(ENUM, TYPE) \
    VisitResult operator()(const TYPE& data) override;
    DAWN_REPLAY_RESOURCE_DATA_MAP(DAWN_REPLAY_RESOURCE_VISITOR_OVERRIDE)
#undef DAWN_REPLAY_RESOURCE_VISITOR_OVERRIDE

  private:
    template <typename T>
    VisitResult Create(const T& data);

    DawnRootCommandVisitor* mVisitor;
};

class DawnRootCommandVisitor : public RootCommandVisitor {
  public:
    using RootCommandVisitor::operator();
    explicit DawnRootCommandVisitor(wgpu::Device device)
        : mDevice(device), mResourceVisitor(this) {}
    ~DawnRootCommandVisitor() override = default;

    void SetSurfaces(const std::vector<wgpu::Surface>& surfaces,
                     const std::vector<schema::ObjectId>& capturedSurfaceIds) {
        DAWN_ASSERT(surfaces.size() >= capturedSurfaceIds.size());
        auto it = surfaces.begin();
        for (auto id : capturedSurfaceIds) {
            mSurfaceIds[id] = *it++;
        }
    }

    VisitResult operator()(const CreateResourceData& data) override {
        mCurrentResourceId = data.resource.id;
        mCurrentResourceLabel = data.resource.label;
        return std::visit(mResourceVisitor, data.data);
    }

    VisitResult operator()(const schema::RootCommandWriteBufferCmdData& data) override;
    VisitResult operator()(const schema::RootCommandWriteTextureCmdData& data) override;
    VisitResult operator()(const schema::RootCommandQueueSubmitCmdData& data) override;
    VisitResult operator()(const schema::RootCommandSetLabelCmdData& data) override;
    VisitResult operator()(const schema::RootCommandInitTextureCmdData& data) override;
    VisitResult operator()(const schema::RootCommandSurfaceConfigureCmdData& data) override;
    VisitResult operator()(const schema::RootCommandSurfaceUnconfigureCmdData& data) override;
    VisitResult operator()(const schema::RootCommandSurfacePresentCmdData& data) override;
    VisitResult operator()(const schema::RootCommandSurfaceGetCurrentTextureCmdData& data) override;
    VisitResult operator()(const schema::RootCommandEndCmdData& data) override {
        return VisitStatus::Continue;
    }

    ResourceVisitor& GetResourceVisitor() override { return mResourceVisitor; }

    wgpu::Device GetDevice() const { return mDevice; }
    schema::ObjectId GetCurrentResourceId() const { return mCurrentResourceId; }
    const std::string& GetCurrentResourceLabel() const { return mCurrentResourceLabel; }

    template <typename T>
    T GetObjectById(schema::ObjectId id) const {
        if (id == 0) {
            return nullptr;
        }
        if constexpr (std::is_same_v<T, wgpu::Surface>) {
            auto iter = mSurfaceIds.find(id);
            if (iter == mSurfaceIds.end()) {
                return nullptr;
            }
            return iter->second;
        } else {
            auto iter = mResources.find(id);
            if (iter == mResources.end()) {
                return nullptr;
            }
            const T* p = std::get_if<T>(&iter->second.resource);
            return p ? *p : nullptr;
        }
    }

    template <typename T>
    T GetObjectByLabel(std::string_view label) const {
        auto isLabel = [label](const LabeledResource& res) { return res.label == label; };
        auto resourcesWithMachingLabel =
            mResources | std::views::values | std::views::filter(isLabel);
        for (const auto& res : resourcesWithMachingLabel) {
            const T* p = std::get_if<T>(&res.resource);
            if (p) {
                return *p;
            }
        }
        return nullptr;
    }

    template <typename T>
    const std::string& GetLabel(const T& object) const {
        if (object == nullptr) {
            return kNotFound;
        }
        return GetLabel(GetObjectId(object));
    }

    const std::string& GetLabel(schema::ObjectId id) const {
        auto iter = mResources.find(id);
        if (iter == mResources.end()) {
            return kNotFound;
        }
        return iter->second.label;
    }

    template <typename T>
    schema::ObjectId GetObjectId(const T& object) const {
        auto iter = mResourceToIdMap.find(object.Get());
        return iter != mResourceToIdMap.end() ? iter->second : 0;
    }

    template <typename T>
    void AddResource(schema::ObjectId id, const std::string& label, T resource) {
        mResources.emplace(id, LabeledResource{label, resource});
        mResourceToIdMap.emplace(resource.Get(), id);
    }

    template <typename T>
    void OverwriteResource(schema::ObjectId id, const std::string& label, T resource) {
        auto iter = mResources.find(id);
        if (iter != mResources.end()) {
            std::visit(
                [&](auto& r) {
                    if constexpr (!std::is_same_v<std::decay_t<decltype(r)>, std::monostate>) {
                        mResourceToIdMap.erase(r.Get());
                    }
                },
                iter->second.resource);
            mResources.erase(iter);
        }
        AddResource(id, label, resource);
    }

    void AddResource(schema::ObjectId id, const std::string& label, Resource resource) {
        std::visit(
            [&](auto& r) {
                if constexpr (!std::is_same_v<std::decay_t<decltype(r)>, std::monostate>) {
                    mResources.emplace(id, LabeledResource{label, r});
                    mResourceToIdMap.emplace(r.Get(), id);
                }
            },
            resource);
    }

    MaybeError SetLabel(schema::ObjectId id, schema::ObjectType type, const std::string& label);

    BlitBufferToDepthTexture& GetBlitBufferToDepthTexture() { return mBlitBufferToDepthTexture; }

    void SetContentReadHead(ReadHead* readHead) override { mContentReadHead = readHead; }

  private:
    wgpu::Device mDevice;
    DawnResourceVisitor mResourceVisitor;

    using IdToResourceMap = absl::flat_hash_map<schema::ObjectId, LabeledResource>;
    IdToResourceMap mResources;

    using ResourcePtrToIdMap = absl::flat_hash_map<const void*, schema::ObjectId>;
    ResourcePtrToIdMap mResourceToIdMap;

    absl::flat_hash_map<schema::ObjectId, wgpu::Surface> mSurfaceIds;

    BlitBufferToDepthTexture mBlitBufferToDepthTexture;

    schema::ObjectId mCurrentResourceId;
    std::string mCurrentResourceLabel;

    ReadHead* mContentReadHead = nullptr;

    std::string kNotFound;
};

namespace {

bool IsSwizzleIdentity(const wgpu::TextureComponentSwizzle& swizzle) {
    return (swizzle.r == wgpu::ComponentSwizzle::R ||
            swizzle.r == wgpu::ComponentSwizzle::Undefined) &&
           (swizzle.g == wgpu::ComponentSwizzle::G ||
            swizzle.g == wgpu::ComponentSwizzle::Undefined) &&
           (swizzle.b == wgpu::ComponentSwizzle::B ||
            swizzle.b == wgpu::ComponentSwizzle::Undefined) &&
           (swizzle.a == wgpu::ComponentSwizzle::A ||
            swizzle.a == wgpu::ComponentSwizzle::Undefined);
}

wgpu::Origin3D ToWGPU(const schema::Origin3D& origin) {
    return wgpu::Origin3D{
        .x = origin.x,
        .y = origin.y,
        .z = origin.z,
    };
}

wgpu::Origin2D ToWGPU(const schema::Origin2D& origin) {
    return wgpu::Origin2D{
        .x = origin.x,
        .y = origin.y,
    };
}

wgpu::Extent3D ToWGPU(const schema::Extent3D& extent) {
    return wgpu::Extent3D{
        .width = extent.width,
        .height = extent.height,
        .depthOrArrayLayers = extent.depthOrArrayLayers,
    };
}

wgpu::Extent2D ToWGPU(const schema::Extent2D& extent) {
    return wgpu::Extent2D{
        .width = extent.width,
        .height = extent.height,
    };
}

wgpu::Color ToWGPU(const schema::Color& color) {
    return wgpu::Color{
        .r = color.r,
        .g = color.g,
        .b = color.b,
        .a = color.a,
    };
}

wgpu::PassTimestampWrites ToWGPU(const DawnRootCommandVisitor& replay,
                                 const schema::TimestampWrites& writes) {
    return wgpu::PassTimestampWrites{
        .nextInChain = nullptr,
        .querySet = replay.GetObjectById<wgpu::QuerySet>(writes.querySetId),
        .beginningOfPassWriteIndex = writes.beginningOfPassWriteIndex,
        .endOfPassWriteIndex = writes.endOfPassWriteIndex,
    };
}

std::vector<wgpu::ConstantEntry> ToWGPU(const std::vector<schema::PipelineConstant>& constants) {
    std::vector<wgpu::ConstantEntry> entries;
    entries.reserve(constants.size());
    std::transform(constants.begin(), constants.end(), std::back_inserter(entries),
                   [](const auto& constant) {
                       return wgpu::ConstantEntry{
                           .key = wgpu::StringView(constant.name),
                           .value = constant.value,
                       };
                   });
    return entries;
}

wgpu::TexelCopyBufferLayout ToWGPU(const schema::TexelCopyBufferLayout& info) {
    return wgpu::TexelCopyBufferLayout{
        .offset = info.offset,
        .bytesPerRow = info.bytesPerRow,
        .rowsPerImage = info.rowsPerImage,
    };
}

wgpu::TexelCopyBufferInfo ToWGPU(const DawnRootCommandVisitor& replay,
                                 const schema::TexelCopyBufferInfo& info) {
    return wgpu::TexelCopyBufferInfo{
        .layout = ToWGPU(info.layout),
        .buffer = replay.GetObjectById<wgpu::Buffer>(info.bufferId),
    };
}

wgpu::TexelCopyTextureInfo ToWGPU(const DawnRootCommandVisitor& replay,
                                  const schema::TexelCopyTextureInfo& info) {
    return wgpu::TexelCopyTextureInfo{
        .texture = replay.GetObjectById<wgpu::Texture>(info.textureId),
        .mipLevel = info.mipLevel,
        .origin = ToWGPU(info.origin),
        .aspect = static_cast<wgpu::TextureAspect>(info.aspect),
    };
}

wgpu::StencilFaceState ToWGPU(const schema::StencilFaceState& state) {
    return wgpu::StencilFaceState{
        .compare = state.compare,
        .failOp = state.failOp,
        .depthFailOp = state.depthFailOp,
        .passOp = state.passOp,
    };
}

wgpu::BlendComponent ToWGPU(const schema::BlendComponent& component) {
    return wgpu::BlendComponent{
        .operation = component.operation,
        .srcFactor = component.srcFactor,
        .dstFactor = component.dstFactor,
    };
}

wgpu::BlendState ToWGPU(const schema::BlendState& state) {
    return wgpu::BlendState{
        .color = ToWGPU(state.color),
        .alpha = ToWGPU(state.alpha),
    };
}

bool IsBlendComponentEnabled(const wgpu::BlendComponent& component) {
    return component.operation != wgpu::BlendOperation::Add ||
           component.srcFactor != wgpu::BlendFactor::One ||
           component.dstFactor != wgpu::BlendFactor::Zero;
}

bool IsBlendEnabled(const wgpu::BlendState& blend) {
    return IsBlendComponentEnabled(blend.color) || IsBlendComponentEnabled(blend.alpha);
}

MaybeError ReadContentIntoBuffer(ReadHead& readHead,
                                 wgpu::Device device,
                                 wgpu::Buffer buffer,
                                 uint64_t bufferOffset,
                                 uint64_t size) {
    const uint32_t* data;
    DAWN_TRY_ASSIGN(data, readHead.GetData(size));

    device.GetQueue().WriteBuffer(buffer, bufferOffset, data, size);
    return {};
}

MaybeError ReadContentIntoTexture(const DawnRootCommandVisitor& replay,
                                  ReadHead& readHead,
                                  wgpu::Device device,
                                  const schema::RootCommandWriteTextureCmdData& cmdData) {
    const uint64_t dataSize = (cmdData.dataSize + 3) & ~3;

    const uint32_t* data;
    DAWN_TRY_ASSIGN(data, readHead.GetData(dataSize));

    wgpu::TexelCopyTextureInfo dst = ToWGPU(replay, cmdData.destination);
    wgpu::TexelCopyBufferLayout layout = ToWGPU(cmdData.layout);
    wgpu::Extent3D size = ToWGPU(cmdData.size);
    device.GetQueue().WriteTexture(&dst, data, cmdData.dataSize, &layout, &size);
    return {};
}

bool TextureFormatNeedsBlit(wgpu::TextureFormat format, wgpu::TextureAspect aspect) {
    switch (format) {
        case wgpu::TextureFormat::Depth24Plus:
        case wgpu::TextureFormat::Depth32Float:
            return true;
        case wgpu::TextureFormat::Depth24PlusStencil8:
        case wgpu::TextureFormat::Depth32FloatStencil8: {
            return aspect == wgpu::TextureAspect::DepthOnly;
        }
        default:
            return false;
    }
}

MaybeError InitializeTexture(const DawnRootCommandVisitor& replay,
                             BlitBufferToDepthTexture& blitBufferToDepthTexture,
                             ReadHead& readHead,
                             wgpu::Device device,
                             const schema::RootCommandInitTextureCmdData& cmdData) {
    const uint64_t dataSize = (cmdData.dataSize + 3) & ~3;

    const uint32_t* data;
    DAWN_TRY_ASSIGN(data, readHead.GetData(dataSize));

    wgpu::TexelCopyTextureInfo dst = ToWGPU(replay, cmdData.destination);
    wgpu::TexelCopyBufferLayout layout = ToWGPU(cmdData.layout);
    wgpu::Extent3D size = ToWGPU(cmdData.size);

    if (TextureFormatNeedsBlit(dst.texture.GetFormat(), dst.aspect)) {
        DAWN_TRY(blitBufferToDepthTexture.Blit(device, dst, data, cmdData.dataSize, layout, size));
    } else {
        device.GetQueue().WriteTexture(&dst, data, cmdData.dataSize, &layout, &size);
    }
    return {};
}

struct BindGroupEntryVisitor {
    const DawnRootCommandVisitor& replay;
    std::vector<wgpu::BindGroupEntry>& entries;
    std::vector<std::unique_ptr<wgpu::ExternalTextureBindingEntry>>& externalTextureBindingEntries;

    template <typename T>
    VisitResult operator()(const T& data) {
        wgpu::BindGroupEntry entry;
        entry.binding = data.binding;
        DAWN_TRY(FillEntry(entry, data.data));
        entries.push_back(entry);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const std::monostate&) {
        return DAWN_INTERNAL_ERROR("invalid bind group entry");
    }

  private:
    MaybeError FillEntry(wgpu::BindGroupEntry& entry,
                         const schema::BindGroupEntryTypeBufferBindingData& data) {
        entry.buffer = replay.GetObjectById<wgpu::Buffer>(data.bufferId);
        entry.offset = data.offset;
        entry.size = data.size;
        return {};
    }

    MaybeError FillEntry(wgpu::BindGroupEntry& entry,
                         const schema::BindGroupEntryTypeSamplerBindingData& data) {
        entry.sampler = replay.GetObjectById<wgpu::Sampler>(data.samplerId);
        return {};
    }

    MaybeError FillEntry(wgpu::BindGroupEntry& entry,
                         const schema::BindGroupEntryTypeTextureBindingData& data) {
        entry.textureView = replay.GetObjectById<wgpu::TextureView>(data.textureViewId);
        return {};
    }

    MaybeError FillEntry(wgpu::BindGroupEntry& entry,
                         const schema::BindGroupEntryTypeStorageTextureBindingData& data) {
        entry.textureView = replay.GetObjectById<wgpu::TextureView>(data.textureViewId);
        return {};
    }

    MaybeError FillEntry(wgpu::BindGroupEntry& entry,
                         const schema::BindGroupEntryTypeTexelBufferBindingData& data) {
        return DAWN_INTERNAL_ERROR("texel buffer binding not supported");
    }

    MaybeError FillEntry(wgpu::BindGroupEntry& entry,
                         const schema::BindGroupEntryTypeInputAttachmentBindingInfoData& data) {
        entry.textureView = replay.GetObjectById<wgpu::TextureView>(data.textureViewId);
        return {};
    }

    MaybeError FillEntry(wgpu::BindGroupEntry& entry,
                         const schema::BindGroupEntryTypeExternalTextureBindingData& data) {
        if (data.externalTextureId != 0) {
            auto& externalBindingEntryPtr = externalTextureBindingEntries.emplace_back(
                std::make_unique<wgpu::ExternalTextureBindingEntry>());
            externalBindingEntryPtr->externalTexture =
                replay.GetObjectById<wgpu::ExternalTexture>(data.externalTextureId);
            entry.nextInChain = externalBindingEntryPtr.get();
        } else {
            // External texture binding can bind a regular texture view
            DAWN_ASSERT(data.textureViewId != 0);
            entry.textureView = replay.GetObjectById<wgpu::TextureView>(data.textureViewId);
        }
        return {};
    }

    template <typename T>
    MaybeError FillEntry(wgpu::BindGroupEntry& entry, const T&) {
        return {};
    }
};

struct BindGroupLayoutEntryVisitor {
    wgpu::BindGroupLayoutEntry& entry;
    wgpu::ExternalTextureBindingLayout& externalTextureBindingLayout;

    template <typename T>
    VisitResult operator()(const T& data) {
        entry.binding = data.binding.binding;
        entry.visibility = data.binding.visibility;
        entry.bindingArraySize = data.binding.bindingArraySize;
        DAWN_TRY(FillEntry(data.data));
        return VisitStatus::Continue;
    }

    VisitResult operator()(const std::monostate&) {
        return DAWN_INTERNAL_ERROR("invalid bind group layout entry");
    }

  private:
    MaybeError FillEntry(const schema::BindGroupLayoutEntryTypeBufferBindingData& data) {
        entry.buffer = {
            .type = data.type,
            .hasDynamicOffset = data.hasDynamicOffset,
            .minBindingSize = data.minBindingSize,
        };
        return {};
    }

    MaybeError FillEntry(const schema::BindGroupLayoutEntryTypeSamplerBindingData& data) {
        entry.sampler = {.type = data.type};
        return {};
    }

    MaybeError FillEntry(const schema::BindGroupLayoutEntryTypeStorageTextureBindingData& data) {
        entry.storageTexture = {
            .access = data.access,
            .format = data.format,
            .viewDimension = data.viewDimension,
        };
        return {};
    }

    MaybeError FillEntry(const schema::BindGroupLayoutEntryTypeTextureBindingData& data) {
        entry.texture = {
            .sampleType = data.sampleType,
            .viewDimension = data.viewDimension,
            .multisampled = data.multisampled,
        };
        return {};
    }

    MaybeError FillEntry(const schema::BindGroupLayoutEntryTypeExternalTextureBindingData& data) {
        entry.nextInChain = &externalTextureBindingLayout;
        return {};
    }

    template <typename T>
    MaybeError FillEntry(const T&) {
        return DAWN_INTERNAL_ERROR("unhandled bind group layout entry type");
    }
};

ResultOrError<wgpu::BindGroup> CreateResource(const DawnRootCommandVisitor& replay,
                                              wgpu::Device device,
                                              const BindGroupData& data,
                                              const std::string& label) {
    std::vector<wgpu::BindGroupEntry> entries;
    entries.reserve(data.entries.size());
    std::vector<std::unique_ptr<wgpu::ExternalTextureBindingEntry>> externalTextureBindingEntries;

    for (const auto& entry : data.entries) {
        VisitStatus status;
        DAWN_TRY_ASSIGN(status, std::visit(BindGroupEntryVisitor{replay, entries,
                                                                 externalTextureBindingEntries},
                                           entry));
        DAWN_ASSERT(status == VisitStatus::Continue);
    }

    wgpu::BindGroupDescriptor desc{
        .label = wgpu::StringView(label),
        .layout = replay.GetObjectById<wgpu::BindGroupLayout>(data.bg.layoutId),
        .entryCount = entries.size(),
        .entries = entries.data(),
    };
    wgpu::BindGroup bindGroup = device.CreateBindGroup(&desc);
    return {bindGroup};
}

ResultOrError<wgpu::BindGroupLayout> CreateResource(const DawnRootCommandVisitor& replay,
                                                    wgpu::Device device,
                                                    const BindGroupLayoutData& data,
                                                    const std::string& label) {
    std::vector<wgpu::BindGroupLayoutEntry> entries;
    entries.reserve(data.entries.size());

    // External texture binding layouts are chained structs that are set as a pointer within
    // the bind group layout entry. We declare an entry here so that it can be used when needed
    // in each BindGroupLayoutEntry and so it can stay alive until the call to
    // device.CreateBindGroupLayout. Because ExternalTextureBindingLayout is an empty struct,
    // there's no issue with using the same struct multiple times.
    wgpu::ExternalTextureBindingLayout externalTextureBindingLayout;

    for (const auto& entryVariant : data.entries) {
        wgpu::BindGroupLayoutEntry entry;
        VisitStatus status;
        DAWN_TRY_ASSIGN(status,
                        std::visit(BindGroupLayoutEntryVisitor{entry, externalTextureBindingLayout},
                                   entryVariant));
        DAWN_ASSERT(status == VisitStatus::Continue);
        entries.push_back(entry);
    }

    wgpu::BindGroupLayoutDescriptor desc{
        .label = wgpu::StringView(label),
        .entryCount = entries.size(),
        .entries = entries.data(),
    };
    wgpu::BindGroupLayout bindGroupLayout = device.CreateBindGroupLayout(&desc);
    return {bindGroupLayout};
}

ResultOrError<wgpu::Buffer> CreateResource(const DawnRootCommandVisitor&,
                                           wgpu::Device device,
                                           const schema::Buffer& buf,
                                           const std::string& label) {
    wgpu::BufferUsage usage =
        (buf.usage & (wgpu::BufferUsage::MapRead | wgpu::BufferUsage::MapWrite))
            ? buf.usage
            : (buf.usage | wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst);

    // Remap mappable write buffers as CopySrc|CopyDst as we use WriteBuffer to set their contents.
    if (usage == (wgpu::BufferUsage::MapWrite | wgpu::BufferUsage::CopySrc)) {
        usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc;
    }

    wgpu::BufferDescriptor desc{
        .label = wgpu::StringView(label),
        .usage = usage,
        .size = buf.size,
    };
    wgpu::Buffer buffer = device.CreateBuffer(&desc);
    return {buffer};
}

ResultOrError<wgpu::ComputePipeline> CreateResource(const DawnRootCommandVisitor& replay,
                                                    wgpu::Device device,
                                                    const schema::ComputePipeline& pipeline,
                                                    const std::string& label) {
    std::vector<wgpu::ConstantEntry> constants = ToWGPU(pipeline.compute.constants);

    wgpu::ComputePipelineDescriptor desc{
        .label = wgpu::StringView(label),
        .layout = replay.GetObjectById<wgpu::PipelineLayout>(pipeline.layoutId),
        .compute =
            {
                .module = replay.GetObjectById<wgpu::ShaderModule>(pipeline.compute.moduleId),
                .entryPoint = wgpu::StringView(pipeline.compute.entryPoint),
                .constantCount = constants.size(),
                .constants = constants.data(),
            },
    };
    wgpu::ComputePipeline computePipeline = device.CreateComputePipeline(&desc);
    return {computePipeline};
}

ResultOrError<wgpu::ExternalTexture> CreateResource(const DawnRootCommandVisitor& replay,
                                                    wgpu::Device device,
                                                    const schema::ExternalTexture& tex,
                                                    const std::string& label) {
    wgpu::ExternalTextureDescriptor desc{
        .label = wgpu::StringView(label),
        .plane0 = replay.GetObjectById<wgpu::TextureView>(tex.plane0Id),
        .plane1 = replay.GetObjectById<wgpu::TextureView>(tex.plane1Id),
        .cropOrigin = ToWGPU(tex.cropOrigin),
        .cropSize = ToWGPU(tex.cropSize),
        .apparentSize = ToWGPU(tex.apparentSize),
        .doYuvToRgbConversionOnly = tex.doYuvToRgbConversionOnly,
        .yuvToRgbConversionMatrix = tex.yuvToRgbConversionMatrix.data(),
        .srcTransferFunctionParameters = tex.srcTransferFunctionParameters.data(),
        .dstTransferFunctionParameters = tex.dstTransferFunctionParameters.data(),
        .gamutConversionMatrix = tex.gamutConversionMatrix.data(),
        .mirrored = tex.mirrored,
        .rotation = tex.rotation,
    };
    wgpu::ExternalTexture externalTexture = device.CreateExternalTexture(&desc);
    return {externalTexture};
}

ResultOrError<wgpu::PipelineLayout> CreateResource(const DawnRootCommandVisitor& replay,
                                                   wgpu::Device device,
                                                   const schema::PipelineLayout& layout,
                                                   const std::string& label) {
    std::vector<wgpu::BindGroupLayout> bindGroupLayouts;
    for (const auto bindGroupLayoutId : layout.bindGroupLayoutIds) {
        bindGroupLayouts.push_back(replay.GetObjectById<wgpu::BindGroupLayout>(bindGroupLayoutId));
    }

    wgpu::PipelineLayoutDescriptor desc{
        .label = wgpu::StringView(label),
        .bindGroupLayoutCount = bindGroupLayouts.size(),
        .bindGroupLayouts = bindGroupLayouts.data(),
        .immediateSize = layout.immediateSize,
    };
    wgpu::PipelineLayout pipelineLayout = device.CreatePipelineLayout(&desc);
    return {pipelineLayout};
}

ResultOrError<wgpu::QuerySet> CreateResource(const DawnRootCommandVisitor& replay,
                                             wgpu::Device device,
                                             const schema::QuerySet& querySetData,
                                             const std::string& label) {
    wgpu::QuerySetDescriptor desc{
        .label = wgpu::StringView(label),
        .type = querySetData.type,
        .count = querySetData.count,
    };
    wgpu::QuerySet querySet = device.CreateQuerySet(&desc);
    return {querySet};
}

template <typename Base, typename Pass>
struct DawnCommandVisitor : Base {
    const DawnRootCommandVisitor& replay;
    Pass pass;

    DawnCommandVisitor(const DawnRootCommandVisitor& r, Pass p) : replay(r), pass(p) {}

    VisitResult operator()(const schema::CommandBufferCommandSetBindGroupCmdData& data) override {
        pass.SetBindGroup(data.index, replay.GetObjectById<wgpu::BindGroup>(data.bindGroupId),
                          data.dynamicOffsets.size(), data.dynamicOffsets.data());
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandSetImmediatesCmdData& data) override {
        pass.SetImmediates(data.offset, data.data.data(), data.data.size());
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandPushDebugGroupCmdData& data) override {
        pass.PushDebugGroup(wgpu::StringView(data.groupLabel));
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandInsertDebugMarkerCmdData& data) override {
        pass.InsertDebugMarker(wgpu::StringView(data.markerLabel));
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandPopDebugGroupCmdData& data) override {
        pass.PopDebugGroup();
        return VisitStatus::Continue;
    }
};

template <typename Base, typename Pass>
struct DawnRenderVisitor : DawnCommandVisitor<Base, Pass> {
    using DawnCommandVisitor<Base, Pass>::DawnCommandVisitor;
    using DawnCommandVisitor<Base, Pass>::replay;
    using DawnCommandVisitor<Base, Pass>::pass;

    VisitResult operator()(
        const schema::CommandBufferCommandSetRenderPipelineCmdData& data) override {
        pass.SetPipeline(replay.template GetObjectById<wgpu::RenderPipeline>(data.pipelineId));
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandSetVertexBufferCmdData& data) override {
        pass.SetVertexBuffer(data.slot, replay.template GetObjectById<wgpu::Buffer>(data.bufferId),
                             data.offset, data.size);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandSetIndexBufferCmdData& data) override {
        pass.SetIndexBuffer(replay.template GetObjectById<wgpu::Buffer>(data.bufferId), data.format,
                            data.offset, data.size);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandDrawCmdData& data) override {
        pass.Draw(data.vertexCount, data.instanceCount, data.firstVertex, data.firstInstance);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandDrawIndexedCmdData& data) override {
        pass.DrawIndexed(data.indexCount, data.instanceCount, data.firstIndex, data.baseVertex,
                         data.firstInstance);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandDrawIndirectCmdData& data) override {
        pass.DrawIndirect(replay.template GetObjectById<wgpu::Buffer>(data.indirectBufferId),
                          data.indirectOffset);
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandDrawIndexedIndirectCmdData& data) override {
        pass.DrawIndexedIndirect(replay.template GetObjectById<wgpu::Buffer>(data.indirectBufferId),
                                 data.indirectOffset);
        return VisitStatus::Continue;
    }
};

struct DawnRenderBundleVisitor : DawnRenderVisitor<RenderBundleVisitor, wgpu::RenderBundleEncoder> {
    using DawnRenderVisitor::DawnRenderVisitor;

    VisitResult operator()(const schema::CommandBufferCommandEndCmdData& data) override {
        return VisitStatus::Continue;
    }
};

ResultOrError<wgpu::RenderBundle> CreateResource(const DawnRootCommandVisitor& replay,
                                                 wgpu::Device device,
                                                 const RenderBundleData& data,
                                                 const std::string& label) {
    wgpu::RenderBundleEncoderDescriptor desc{
        .colorFormatCount = data.bundle.colorFormats.size(),
        .colorFormats = data.bundle.colorFormats.data(),
        .depthStencilFormat = data.bundle.depthStencilFormat,
        .sampleCount = data.bundle.sampleCount,
        .depthReadOnly = data.bundle.depthReadOnly,
        .stencilReadOnly = data.bundle.stencilReadOnly,
    };
    wgpu::RenderBundleEncoder encoder = device.CreateRenderBundleEncoder(&desc);
    DawnRenderBundleVisitor visitor{replay, encoder};
    VisitStatus status;
    DAWN_TRY_ASSIGN(status, ProcessRenderBundleCommands(data.readHead, &visitor));
    DAWN_ASSERT(status == VisitStatus::Continue);

    wgpu::RenderBundleDescriptor bundleDesc{
        .label = wgpu::StringView(label),
    };
    wgpu::RenderBundle renderBundle = encoder.Finish(&bundleDesc);
    return {renderBundle};
}

ResultOrError<wgpu::RenderPipeline> CreateResource(const DawnRootCommandVisitor& replay,
                                                   wgpu::Device device,
                                                   const schema::RenderPipeline& pipeline,
                                                   const std::string& label) {
    std::vector<wgpu::ConstantEntry> vertexConstants = ToWGPU(pipeline.vertex.program.constants);
    std::vector<wgpu::ConstantEntry> fragmentConstants =
        ToWGPU(pipeline.fragment.program.constants);
    std::vector<wgpu::ColorTargetState> colorTargets;
    std::array<wgpu::ColorTargetStateExpandResolveTextureDawn, kMaxColorAttachments>
        expandResolveChains;
    std::vector<wgpu::BlendState> blendStates(pipeline.fragment.targets.size());
    std::vector<wgpu::VertexBufferLayout> buffers;

    std::vector<wgpu::VertexAttribute> attributes(kMaxVertexAttributes);
    uint32_t attributeCount = 0;

    for (const auto& buffer : pipeline.vertex.buffers) {
        const auto attributesForBuffer = &attributes[attributeCount];
        for (const auto& attrib : buffer.attributes) {
            auto& attr = attributes[attributeCount++];
            attr.format = attrib.format;
            attr.offset = attrib.offset;
            attr.shaderLocation = attrib.shaderLocation;
        }
        buffers.push_back({
            .stepMode = buffer.stepMode,
            .arrayStride = buffer.arrayStride,
            .attributeCount = buffer.attributes.size(),
            .attributes = attributesForBuffer,
        });
    }

    wgpu::FragmentState* fragment = nullptr;
    wgpu::FragmentState fragmentState;
    if (pipeline.fragment.program.moduleId) {
        fragment = &fragmentState;
        fragmentState.module =
            replay.GetObjectById<wgpu::ShaderModule>(pipeline.fragment.program.moduleId);
        fragmentState.entryPoint = wgpu::StringView(pipeline.fragment.program.entryPoint);
        fragmentState.constantCount = fragmentConstants.size();
        fragmentState.constants = fragmentConstants.data();
        for (const auto& target : pipeline.fragment.targets) {
            wgpu::BlendState& blend = blendStates[colorTargets.size()];
            blend = ToWGPU(target.blend);

            wgpu::ChainedStruct* nextInChain = nullptr;
            if (target.expandResolveMode != schema::ExpandResolveMode::Unused) {
                auto expandResolve = &expandResolveChains[colorTargets.size()];
                expandResolve->enabled =
                    target.expandResolveMode == schema::ExpandResolveMode::Enabled;
                nextInChain = expandResolve;
            }
            colorTargets.push_back({
                .nextInChain = nextInChain,
                .format = target.format,
                .blend = IsBlendEnabled(blend) ? &blend : nullptr,
                .writeMask = target.writeMask,
            });
        }
        fragmentState.targetCount = colorTargets.size();
        fragmentState.targets = colorTargets.data();
    }

    wgpu::DepthStencilState* depthStencil = nullptr;
    wgpu::DepthStencilState depthStencilState;
    if (pipeline.depthStencil.format != wgpu::TextureFormat::Undefined) {
        depthStencil = &depthStencilState;
        depthStencilState.format = pipeline.depthStencil.format;
        depthStencilState.depthWriteEnabled = pipeline.depthStencil.depthWriteEnabled;
        depthStencilState.depthCompare = pipeline.depthStencil.depthCompare;
        depthStencilState.stencilFront = ToWGPU(pipeline.depthStencil.stencilFront);
        depthStencilState.stencilBack = ToWGPU(pipeline.depthStencil.stencilBack);
        depthStencilState.stencilReadMask = pipeline.depthStencil.stencilReadMask;
        depthStencilState.stencilWriteMask = pipeline.depthStencil.stencilWriteMask;
        depthStencilState.depthBias = pipeline.depthStencil.depthBias;
        depthStencilState.depthBiasSlopeScale = pipeline.depthStencil.depthBiasSlopeScale;
        depthStencilState.depthBiasClamp = pipeline.depthStencil.depthBiasClamp;
    }

    wgpu::RenderPipelineDescriptor desc{
        .label = wgpu::StringView(label),
        .layout = replay.GetObjectById<wgpu::PipelineLayout>(pipeline.layoutId),
        .vertex =
            {
                .module =
                    replay.GetObjectById<wgpu::ShaderModule>(pipeline.vertex.program.moduleId),
                .entryPoint = wgpu::StringView(pipeline.vertex.program.entryPoint),
                .constantCount = vertexConstants.size(),
                .constants = vertexConstants.data(),
                .bufferCount = buffers.size(),
                .buffers = buffers.data(),
            },
        .primitive =
            {
                .topology = pipeline.primitive.topology,
                .stripIndexFormat = pipeline.primitive.stripIndexFormat,
                .frontFace = pipeline.primitive.frontFace,
                .cullMode = pipeline.primitive.cullMode,
                .unclippedDepth = pipeline.primitive.unclippedDepth,
            },
        .depthStencil = depthStencil,
        .multisample =
            {
                .count = pipeline.multisample.count,
                .mask = pipeline.multisample.mask,
                .alphaToCoverageEnabled = pipeline.multisample.alphaToCoverageEnabled,
            },
        .fragment = fragment,
    };
    wgpu::RenderPipeline renderPipeline = device.CreateRenderPipeline(&desc);
    return {renderPipeline};
}

ResultOrError<wgpu::Sampler> CreateResource(const DawnRootCommandVisitor&,
                                            wgpu::Device device,
                                            const schema::Sampler& sampler,
                                            const std::string& label) {
    wgpu::SamplerDescriptor desc{
        .label = wgpu::StringView(label),
        .addressModeU = sampler.addressModeU,
        .addressModeV = sampler.addressModeV,
        .addressModeW = sampler.addressModeW,
        .magFilter = sampler.magFilter,
        .minFilter = sampler.minFilter,
        .mipmapFilter = sampler.mipmapFilter,
        .lodMinClamp = sampler.lodMinClamp,
        .lodMaxClamp = sampler.lodMaxClamp,
        .compare = sampler.compare,
        .maxAnisotropy = sampler.maxAnisotropy,
    };
    return {device.CreateSampler(&desc)};
}

ResultOrError<wgpu::ShaderModule> CreateResource(const DawnRootCommandVisitor&,
                                                 wgpu::Device device,
                                                 const schema::ShaderModule& mod,
                                                 const std::string& label) {
    // TODO(452840621): Make this use a chain instead of hard coded to WGSL only and handle other
    // chained structs.
    wgpu::ShaderSourceWGSL source({
        .nextInChain = nullptr,
        .code = wgpu::StringView(mod.code),
    });
    wgpu::ShaderModuleDescriptor desc{
        .nextInChain = &source,
        .label = wgpu::StringView(label),
    };
    wgpu::ShaderModule shaderModule = device.CreateShaderModule(&desc);
    return {shaderModule};
}

ResultOrError<wgpu::TexelBufferView> CreateResource(const DawnRootCommandVisitor&,
                                                    wgpu::Device device,
                                                    const schema::TexelBufferView& view,
                                                    const std::string& label) {
    return DAWN_UNIMPLEMENTED_ERROR("TexelBufferView not supported");
}

ResultOrError<wgpu::Texture> CreateResource(const DawnRootCommandVisitor&,
                                            wgpu::Device device,
                                            const schema::Texture& tex,
                                            const std::string& label) {
    wgpu::TextureUsage usage = tex.usage;
    if (!(usage & wgpu::TextureUsage::TransientAttachment)) {
        usage |= wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst;
    }

    wgpu::TextureDescriptor desc{
        .label = wgpu::StringView(label),
        .usage = usage,
        .dimension = tex.dimension,
        .size = ToWGPU(tex.size),
        .format = tex.format,
        .mipLevelCount = tex.mipLevelCount,
        .sampleCount = tex.sampleCount,
        .viewFormatCount = tex.viewFormats.size(),
        .viewFormats = tex.viewFormats.data(),
    };
    wgpu::Texture texture = device.CreateTexture(&desc);
    return {texture};
}

ResultOrError<wgpu::TextureView> CreateResource(const DawnRootCommandVisitor& replay,
                                                wgpu::Device device,
                                                const schema::TextureView& view,
                                                const std::string& label) {
    wgpu::TextureViewDescriptor desc{
        .label = wgpu::StringView(label),
        .format = view.format,
        .dimension = view.dimension,
        .baseMipLevel = view.baseMipLevel,
        .mipLevelCount = view.mipLevelCount,
        .baseArrayLayer = view.baseArrayLayer,
        .arrayLayerCount = view.arrayLayerCount,
        .aspect = view.aspect,
        .usage = view.usage,
    };

    wgpu::TextureComponentSwizzleDescriptor swizzleDesc = {};
    swizzleDesc.swizzle.r = view.swizzle.r;
    swizzleDesc.swizzle.g = view.swizzle.g;
    swizzleDesc.swizzle.b = view.swizzle.b;
    swizzleDesc.swizzle.a = view.swizzle.a;
    if (!IsSwizzleIdentity(swizzleDesc.swizzle)) {
        desc.nextInChain = &swizzleDesc;
    }

    wgpu::Texture texture = replay.GetObjectById<wgpu::Texture>(view.textureId);
    wgpu::TextureView textureView = texture.CreateView(&desc);
    return {textureView};
}

struct DawnComputePassVisitor : DawnCommandVisitor<ComputePassVisitor, wgpu::ComputePassEncoder> {
    using DawnCommandVisitor::DawnCommandVisitor;

    VisitResult operator()(
        const schema::CommandBufferCommandSetComputePipelineCmdData& data) override {
        pass.SetPipeline(replay.GetObjectById<wgpu::ComputePipeline>(data.pipelineId));
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandDispatchCmdData& data) override {
        pass.DispatchWorkgroups(data.x, data.y, data.z);
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandDispatchIndirectCmdData& data) override {
        pass.DispatchWorkgroupsIndirect(replay.GetObjectById<wgpu::Buffer>(data.bufferId),
                                        data.offset);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandWriteTimestampCmdData& data) override {
        pass.WriteTimestamp(replay.GetObjectById<wgpu::QuerySet>(data.querySetId), data.queryIndex);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandEndCmdData& data) override {
        pass.End();
        return VisitStatus::Continue;
    }
};

struct DawnRenderPassVisitor : DawnRenderVisitor<RenderPassVisitor, wgpu::RenderPassEncoder> {
    using DawnRenderVisitor::DawnRenderVisitor;

    VisitResult operator()(const schema::CommandBufferCommandExecuteBundlesCmdData& data) override {
        std::vector<wgpu::RenderBundle> bundles;
        for (auto bundleId : data.bundleIds) {
            bundles.push_back(replay.GetObjectById<wgpu::RenderBundle>(bundleId));
        }
        pass.ExecuteBundles(bundles.size(), bundles.data());
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandBeginOcclusionQueryCmdData& data) override {
        pass.BeginOcclusionQuery(data.queryIndex);
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandEndOcclusionQueryCmdData& data) override {
        pass.EndOcclusionQuery();
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandSetBlendConstantCmdData& data) override {
        wgpu::Color color = ToWGPU(data.color);
        pass.SetBlendConstant(&color);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandSetScissorRectCmdData& data) override {
        pass.SetScissorRect(data.x, data.y, data.width, data.height);
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandSetStencilReferenceCmdData& data) override {
        pass.SetStencilReference(data.reference);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandSetViewportCmdData& data) override {
        pass.SetViewport(data.x, data.y, data.width, data.height, data.minDepth, data.maxDepth);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandWriteTimestampCmdData& data) override {
        pass.WriteTimestamp(replay.GetObjectById<wgpu::QuerySet>(data.querySetId), data.queryIndex);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandEndCmdData& data) override {
        pass.End();
        return VisitStatus::Continue;
    }
};

struct DawnEncoderVisitor : EncoderVisitor {
    const DawnRootCommandVisitor& replay;
    wgpu::CommandEncoder pass;
    wgpu::Device device;

    DawnEncoderVisitor(const DawnRootCommandVisitor& r, wgpu::CommandEncoder p, wgpu::Device d)
        : replay(r), pass(p), device(d) {}

    ResultOrError<ComputePassVisitor*> BeginComputePass(
        const schema::CommandBufferCommandBeginComputePassCmdData& data) override {
        wgpu::PassTimestampWrites timestampWrites = ToWGPU(replay, data.timestampWrites);
        wgpu::ComputePassDescriptor desc{
            .label = wgpu::StringView(data.label),
            .timestampWrites = timestampWrites.querySet ? &timestampWrites : nullptr,
        };
        mComputePassEncoder = pass.BeginComputePass(&desc);
        mComputePassVisitor = std::make_unique<DawnComputePassVisitor>(replay, mComputePassEncoder);
        return mComputePassVisitor.get();
    }

    ResultOrError<RenderPassVisitor*> BeginRenderPass(
        const schema::CommandBufferCommandBeginRenderPassCmdData& data) override {
        wgpu::PassTimestampWrites timestampWrites = ToWGPU(replay, data.timestampWrites);
        std::vector<wgpu::RenderPassColorAttachment> colorAttachments;

        for (const auto& attachment : data.colorAttachments) {
            colorAttachments.push_back(wgpu::RenderPassColorAttachment{
                .nextInChain = nullptr,
                .view = replay.GetObjectById<wgpu::TextureView>(attachment.viewId),
                .depthSlice = attachment.depthSlice,
                .resolveTarget =
                    replay.GetObjectById<wgpu::TextureView>(attachment.resolveTargetId),
                .loadOp = attachment.loadOp,
                .storeOp = attachment.storeOp,
                .clearValue = ToWGPU(attachment.clearValue),
            });
        }

        wgpu::RenderPassDepthStencilAttachment depthStencilAttachment{
            .view = replay.GetObjectById<wgpu::TextureView>(data.depthStencilAttachment.viewId),
            .depthLoadOp = data.depthStencilAttachment.depthLoadOp,
            .depthStoreOp = data.depthStencilAttachment.depthStoreOp,
            .depthClearValue = data.depthStencilAttachment.depthClearValue,
            .depthReadOnly = data.depthStencilAttachment.depthReadOnly,
            .stencilLoadOp = data.depthStencilAttachment.stencilLoadOp,
            .stencilStoreOp = data.depthStencilAttachment.stencilStoreOp,
            .stencilClearValue = data.depthStencilAttachment.stencilClearValue,
            .stencilReadOnly = data.depthStencilAttachment.stencilReadOnly,
        };

        wgpu::RenderPassDescriptor desc{
            .label = wgpu::StringView(data.label),
            .colorAttachmentCount = colorAttachments.size(),
            .colorAttachments = colorAttachments.data(),
            .depthStencilAttachment =
                depthStencilAttachment.view != nullptr ? &depthStencilAttachment : nullptr,
            .occlusionQuerySet = replay.GetObjectById<wgpu::QuerySet>(data.occlusionQuerySetId),
            .timestampWrites = timestampWrites.querySet ? &timestampWrites : nullptr,
        };

        wgpu::RenderPassDescriptorResolveRect resolveRect;
        if (data.resolveRect.width != 0) {
            resolveRect.colorOffsetX = data.resolveRect.colorOffsetX;
            resolveRect.colorOffsetY = data.resolveRect.colorOffsetY;
            resolveRect.resolveOffsetX = data.resolveRect.resolveOffsetX;
            resolveRect.resolveOffsetY = data.resolveRect.resolveOffsetY;
            resolveRect.width = data.resolveRect.width;
            resolveRect.height = data.resolveRect.height;
            desc.nextInChain = &resolveRect;
        }

        mRenderPassEncoder = pass.BeginRenderPass(&desc);
        mRenderPassVisitor = std::make_unique<DawnRenderPassVisitor>(replay, mRenderPassEncoder);
        return mRenderPassVisitor.get();
    }

    VisitResult operator()(const schema::CommandBufferCommandClearBufferCmdData& data) override {
        pass.ClearBuffer(replay.GetObjectById<wgpu::Buffer>(data.bufferId), data.offset, data.size);
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandCopyBufferToBufferCmdData& data) override {
        pass.CopyBufferToBuffer(
            replay.GetObjectById<wgpu::Buffer>(data.srcBufferId), data.srcOffset,
            replay.GetObjectById<wgpu::Buffer>(data.dstBufferId), data.dstOffset, data.size);
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandCopyBufferToTextureCmdData& data) override {
        wgpu::TexelCopyBufferInfo src = ToWGPU(replay, data.source);
        wgpu::TexelCopyTextureInfo dst = ToWGPU(replay, data.destination);
        wgpu::Extent3D copySize = ToWGPU(data.copySize);
        pass.CopyBufferToTexture(&src, &dst, &copySize);
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandCopyTextureToBufferCmdData& data) override {
        wgpu::TexelCopyTextureInfo src = ToWGPU(replay, data.source);
        wgpu::TexelCopyBufferInfo dst = ToWGPU(replay, data.destination);
        wgpu::Extent3D copySize = ToWGPU(data.copySize);
        pass.CopyTextureToBuffer(&src, &dst, &copySize);
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandCopyTextureToTextureCmdData& data) override {
        wgpu::TexelCopyTextureInfo src = ToWGPU(replay, data.source);
        wgpu::TexelCopyTextureInfo dst = ToWGPU(replay, data.destination);
        wgpu::Extent3D copySize = ToWGPU(data.copySize);
        pass.CopyTextureToTexture(&src, &dst, &copySize);
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandResolveQuerySetCmdData& data) override {
        pass.ResolveQuerySet(
            replay.GetObjectById<wgpu::QuerySet>(data.querySetId), data.firstQuery, data.queryCount,
            replay.GetObjectById<wgpu::Buffer>(data.destinationId), data.destinationOffset);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandWriteBufferCmdData& data) override {
        wgpu::Buffer buffer = replay.GetObjectById<wgpu::Buffer>(data.bufferId);
        pass.WriteBuffer(buffer, data.bufferOffset, data.data.data(), data.data.size());
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandWriteTimestampCmdData& data) override {
        pass.WriteTimestamp(replay.GetObjectById<wgpu::QuerySet>(data.querySetId), data.queryIndex);
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandPushDebugGroupCmdData& data) override {
        pass.PushDebugGroup(wgpu::StringView(data.groupLabel));
        return VisitStatus::Continue;
    }

    VisitResult operator()(
        const schema::CommandBufferCommandInsertDebugMarkerCmdData& data) override {
        pass.InsertDebugMarker(wgpu::StringView(data.markerLabel));
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandPopDebugGroupCmdData& data) override {
        pass.PopDebugGroup();
        return VisitStatus::Continue;
    }

    VisitResult operator()(const schema::CommandBufferCommandEndCmdData& data) override {
        return VisitStatus::Continue;
    }

  private:
    wgpu::ComputePassEncoder mComputePassEncoder;
    std::unique_ptr<DawnComputePassVisitor> mComputePassVisitor;
    wgpu::RenderPassEncoder mRenderPassEncoder;
    std::unique_ptr<DawnRenderPassVisitor> mRenderPassVisitor;
};

ResultOrError<wgpu::CommandBuffer> CreateResource(const DawnRootCommandVisitor& replay,
                                                  wgpu::Device device,
                                                  const CommandBufferData& data,
                                                  const std::string& label) {
    wgpu::CommandEncoderDescriptor desc{
        .label = wgpu::StringView(label),
    };
    wgpu::CommandEncoder encoder = device.CreateCommandEncoder(&desc);
    DawnEncoderVisitor visitor{replay, encoder, device};
    VisitStatus status;
    DAWN_TRY_ASSIGN(status, ProcessEncoderCommands(data.readHead, &visitor));
    DAWN_ASSERT(status == VisitStatus::Continue);
    return {encoder.Finish()};
}

ResultOrError<wgpu::CommandBuffer> CreateResource(const DawnRootCommandVisitor& replay,
                                                  wgpu::Device device,
                                                  const DeviceData& data,
                                                  const std::string& label) {
    // This is here to make resource handling generic. We don't create
    // devices from commands but we reference devices as another resource.
    return DAWN_INTERNAL_ERROR("Device creation not supported");
}

ResultOrError<wgpu::Surface> CreateResource(const DawnRootCommandVisitor& replay,
                                            wgpu::Device device,
                                            const InvalidData& data,
                                            const std::string& label) {
    // This is here to make resource handling generic. We don't create
    // surfaces from this CreateResource but from SurfaceCreate root command.
    return DAWN_INTERNAL_ERROR("Surface creation not supported through CreateResource");
}

}  // anonymous namespace

#define DAWN_REPLAY_VISIT_RESOURCE_CREATE(ENUM, TYPE)               \
    VisitResult DawnResourceVisitor::operator()(const TYPE& data) { \
        return Create(data);                                        \
    }
DAWN_REPLAY_RESOURCE_DATA_MAP(DAWN_REPLAY_VISIT_RESOURCE_CREATE)
#undef DAWN_REPLAY_VISIT_RESOURCE_CREATE

template <typename T>
VisitResult DawnResourceVisitor::Create(const T& data) {
    auto res =
        CreateResource(*mVisitor, mVisitor->GetDevice(), data, mVisitor->GetCurrentResourceLabel());
    if (res.IsError()) {
        return {res.AcquireError()};
    }
    mVisitor->AddResource(mVisitor->GetCurrentResourceId(), mVisitor->GetCurrentResourceLabel(),
                          res.AcquireSuccess());
    return VisitStatus::Continue;
}

VisitResult DawnRootCommandVisitor::operator()(const schema::RootCommandWriteBufferCmdData& data) {
    wgpu::Buffer buffer = GetObjectById<wgpu::Buffer>(data.bufferId);
    DAWN_TRY(
        ReadContentIntoBuffer(*mContentReadHead, mDevice, buffer, data.bufferOffset, data.size));
    return VisitStatus::Continue;
}

VisitResult DawnRootCommandVisitor::operator()(const schema::RootCommandWriteTextureCmdData& data) {
    DAWN_TRY(ReadContentIntoTexture(*this, *mContentReadHead, mDevice, data));
    return VisitStatus::Continue;
}

VisitResult DawnRootCommandVisitor::operator()(const schema::RootCommandQueueSubmitCmdData& data) {
    std::vector<wgpu::CommandBuffer> commandBuffers;
    commandBuffers.reserve(data.commandBuffers.size());
    for (auto id : data.commandBuffers) {
        commandBuffers.push_back(GetObjectById<wgpu::CommandBuffer>(id));
    }
    mDevice.GetQueue().Submit(commandBuffers.size(), commandBuffers.data());
    return VisitStatus::Continue;
}

VisitResult DawnRootCommandVisitor::operator()(const schema::RootCommandSetLabelCmdData& data) {
    DAWN_TRY(SetLabel(data.id, data.type, data.label));
    return VisitStatus::Continue;
}

VisitResult DawnRootCommandVisitor::operator()(const schema::RootCommandInitTextureCmdData& data) {
    DAWN_TRY(InitializeTexture(*this, mBlitBufferToDepthTexture, *mContentReadHead, mDevice, data));
    return VisitStatus::Continue;
}

VisitResult DawnRootCommandVisitor::operator()(
    const schema::RootCommandSurfaceConfigureCmdData& data) {
    wgpu::Surface surface = GetObjectById<wgpu::Surface>(data.surfaceId);
    DAWN_ASSERT(surface);
    wgpu::Device device = GetObjectById<wgpu::Device>(data.config.deviceId);

    wgpu::SurfaceConfiguration config = {};
    config.device = device;
    config.format = data.config.format;
    config.usage = data.config.usage;
    config.viewFormatCount = data.config.viewFormats.size();
    config.viewFormats = data.config.viewFormats.data();
    config.alphaMode = data.config.alphaMode;
    config.width = data.config.width;
    config.height = data.config.height;
    config.presentMode = data.config.presentMode;

    surface.Configure(&config);

    return VisitStatus::Continue;
}

VisitResult DawnRootCommandVisitor::operator()(
    const schema::RootCommandSurfaceUnconfigureCmdData& data) {
    wgpu::Surface surface = GetObjectById<wgpu::Surface>(data.surfaceId);
    DAWN_ASSERT(surface);
    surface.Unconfigure();

    return VisitStatus::Continue;
}

VisitResult DawnRootCommandVisitor::operator()(
    const schema::RootCommandSurfacePresentCmdData& data) {
    wgpu::Surface surface = GetObjectById<wgpu::Surface>(data.surfaceId);
    DAWN_ASSERT(surface);
    surface.Present();
    return VisitStatus::Stop;
}

VisitResult DawnRootCommandVisitor::operator()(
    const schema::RootCommandSurfaceGetCurrentTextureCmdData& data) {
    wgpu::Surface surface = GetObjectById<wgpu::Surface>(data.surfaceId);
    DAWN_ASSERT(surface);
    wgpu::SurfaceTexture surfaceTexture = {};
    surface.GetCurrentTexture(&surfaceTexture);

    if (surfaceTexture.status == wgpu::SurfaceGetCurrentTextureStatus::SuccessOptimal ||
        surfaceTexture.status == wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal) {
        std::string label = GetLabel(data.textureId);
        OverwriteResource(data.textureId, label, surfaceTexture.texture);
    }
    return VisitStatus::Continue;
}

MaybeError DawnRootCommandVisitor::SetLabel(schema::ObjectId id,
                                            schema::ObjectType type,
                                            const std::string& label) {
// We update both the object's label and our own copy of the label
// as there is no API to get an object's label from WebGPU
#define DAWN_SET_LABEL_CASE(NAME)                                                           \
    case schema::ObjectType::NAME: {                                                        \
        auto iter = mResources.find(id);                                                    \
        DAWN_ASSERT(iter != mResources.end());                                              \
        std::get_if<wgpu::NAME>(&iter->second.resource)->SetLabel(wgpu::StringView(label)); \
        iter->second.label = label;                                                         \
        break;                                                                              \
    }

#define DAWN_SET_LABEL_GEN(NAME, MEMBERS) MEMBERS(DAWN_SET_LABEL_CASE)

    switch (type) {
        DAWN_REPLAY_OBJECT_TYPES_ENUM(DAWN_SET_LABEL_GEN)
        default:
            return DAWN_INTERNAL_ERROR("unhandled resource type");
    }

#undef DAWN_SET_LABEL_GEN
#undef DAWN_SET_LABEL_CASE

    return {};
}

std::unique_ptr<ReplayImpl> ReplayImpl::Create(wgpu::Device device,
                                               std::unique_ptr<Capture> capture) {
    auto captureImpl = std::unique_ptr<CaptureImpl>(static_cast<CaptureImpl*>(capture.release()));
    return std::unique_ptr<ReplayImpl>(new ReplayImpl(device, std::move(captureImpl)));
}

ReplayImpl::ReplayImpl(wgpu::Device device, std::unique_ptr<CaptureImpl> capture)
    : mVisitor(new DawnRootCommandVisitor(device)),
      mCapture(std::move(capture)),
      mCommandReadHead(mCapture->GetCommandReadHead()),
      mContentReadHead(mCapture->GetContentReadHead()) {
    mVisitor->AddResource(schema::kDeviceId, "", device);
}

ReplayImpl::~ReplayImpl() = default;

void ReplayImpl::SetSurfaces(std::vector<wgpu::Surface> surfaces) {
    SurfaceDiscoveryVisitor discovery;
    mCapture->Walk(discovery);
    mVisitor->SetSurfaces(surfaces, discovery.GetSurfaceIds());
}

Capture* ReplayImpl::GetCapture() const {
    return mCapture.get();
}

MaybeError ReplayImpl::Play() {
    ReplayStatus status = ReplayStatus::Continuing;
    while (status == ReplayStatus::Continuing) {
        DAWN_TRY_ASSIGN(status, PlayFrame());
    }
    return {};
}

ResultOrError<ReplayStatus> ReplayImpl::PlayFrame() {
    if (mCommandReadHead.IsDone()) {
        return ReplayStatus::Finished;
    }

    DAWN_TRY(mCapture->Walk(*mVisitor, &mCommandReadHead, &mContentReadHead));

    return ReplayStatus::Continuing;
}

template <typename T>
T ReplayImpl::GetObjectByLabel(std::string_view label) const {
    return mVisitor->GetObjectByLabel<T>(label);
}

template <typename T>
T ReplayImpl::GetObjectById(schema::ObjectId id) const {
    return mVisitor->GetObjectById<T>(id);
}

template <typename T>
const std::string& ReplayImpl::GetLabel(const T& object) const {
    return mVisitor->GetLabel(object);
}

const std::string& ReplayImpl::GetLabel(schema::ObjectId id) const {
    return mVisitor->GetLabel(id);
}

}  // namespace dawn::replay
