/*
 * Copyright 2019 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "include/core/SkTypes.h"

#if defined(SK_GANESH) && defined(SK_VULKAN)
#include "include/core/SkAlphaType.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColorSpace.h"
#include "include/core/SkColorType.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkSurface.h"
#include "include/core/SkTypes.h"
#include "include/gpu/GpuTypes.h"
#include "include/gpu/ganesh/GrBackendSurface.h"
#include "include/gpu/ganesh/GrDirectContext.h"
#include "include/gpu/ganesh/GrTypes.h"
#include "include/gpu/ganesh/SkImageGanesh.h"
#include "include/gpu/ganesh/SkSurfaceGanesh.h"
#include "include/gpu/ganesh/vk/GrVkBackendSurface.h"
#include "tests/CtsEnforcement.h"
#include "tests/Test.h"
#include "tools/gpu/vk/VkYcbcrSamplerHelper.h"

#include <vulkan/vulkan_core.h>

#include <cmath>
#include <cstddef>
#include <cstdint>
#include <vector>

class SkImage;
struct GrContextOptions;
const size_t kImageWidth = 8;
const size_t kImageHeight = 8;

static int round_and_clamp(float x) {
    int r = static_cast<int>(round(x));
    if (r > 255) return 255;
    if (r < 0) return 0;
    return r;
}

DEF_GANESH_TEST_FOR_VULKAN_CONTEXT(VkYCbcrSampler_DrawImageWithYcbcrSampler,
                                   reporter,
                                   ctxInfo,
                                   CtsEnforcement::kApiLevel_T) {
    GrDirectContext* dContext = ctxInfo.directContext();

    VkYcbcrSamplerHelper ycbcrHelper(dContext);
    if (!ycbcrHelper.isYCbCrSupported()) {
        return;
    }

    if (!ycbcrHelper.createGrBackendTexture(kImageWidth, kImageHeight)) {
        ERRORF(reporter, "Failed to create I420 backend texture");
        return;
    }

    sk_sp<SkImage> srcImage = SkImages::BorrowTextureFrom(dContext,
                                                          ycbcrHelper.grBackendTexture(),
                                                          kTopLeft_GrSurfaceOrigin,
                                                          kRGB_888x_SkColorType,
                                                          kPremul_SkAlphaType,
                                                          nullptr);
    if (!srcImage) {
        ERRORF(reporter, "Failed to create I420 image");
        return;
    }

    sk_sp<SkSurface> surface = SkSurfaces::RenderTarget(
            dContext,
            skgpu::Budgeted::kNo,
            SkImageInfo::Make(kImageWidth, kImageHeight, kN32_SkColorType, kPremul_SkAlphaType));
    if (!surface) {
        ERRORF(reporter, "Failed to create target SkSurface");
        return;
    }
    surface->getCanvas()->drawImage(srcImage, 0, 0);
    dContext->flushAndSubmit(surface.get());

    std::vector<uint8_t> readbackData(kImageWidth * kImageHeight * 4);
    if (!surface->readPixels(SkImageInfo::Make(kImageWidth, kImageHeight, kRGBA_8888_SkColorType,
                                               kOpaque_SkAlphaType),
                             readbackData.data(), kImageWidth * 4, 0, 0)) {
        ERRORF(reporter, "Readback failed");
        return;
    }

    // Allow resulting color to be off by 1 in each channel as some Vulkan implementations do not
    // round YCbCr sampler result properly.
    const int kColorTolerance = 1;

    // Verify results only for pixels with even coordinates, since others use
    // interpolated U & V channels.
    for (size_t y = 0; y < kImageHeight; y += 2) {
        for (size_t x = 0; x < kImageWidth; x += 2) {
            auto y2 = VkYcbcrSamplerHelper::GetExpectedY(x, y, kImageWidth, kImageHeight);
            auto [u, v] = VkYcbcrSamplerHelper::GetExpectedUV(x, y, kImageWidth, kImageHeight);

            // createI420Image() initializes the image with VK_SAMPLER_YCBCR_RANGE_ITU_NARROW.
            float yChannel = (static_cast<float>(y2) - 16.0) / 219.0;
            float uChannel = (static_cast<float>(u) - 128.0) / 224.0;
            float vChannel = (static_cast<float>(v) - 128.0) / 224.0;

            // BR.709 conversion as specified in
            // https://www.khronos.org/registry/DataFormat/specs/1.2/dataformat.1.2.html#MODEL_YUV
            int expectedR = round_and_clamp((yChannel + 1.5748f * vChannel) * 255.0);
            int expectedG = round_and_clamp((yChannel - 0.13397432f / 0.7152f * uChannel -
                                             0.33480248f / 0.7152f * vChannel) *
                                            255.0);
            int expectedB = round_and_clamp((yChannel + 1.8556f * uChannel) * 255.0);

            int r = readbackData[(y * kImageWidth + x) * 4];
            if (abs(r - expectedR) > kColorTolerance) {
                ERRORF(reporter, "R should be %d, but is %d at (%zu, %zu)", expectedR, r, x, y);
            }

            int g = readbackData[(y * kImageWidth + x) * 4 + 1];
            if (abs(g - expectedG) > kColorTolerance) {
                ERRORF(reporter, "G should be %d, but is %d at (%zu, %zu)", expectedG, g, x, y);
            }

            int b = readbackData[(y * kImageWidth + x) * 4 + 2];
            if (abs(b - expectedB) > kColorTolerance) {
                ERRORF(reporter, "B should be %d, but is %d at (%zu, %zu)", expectedB, b, x, y);
            }
        }
    }
}

// Verifies that it's not possible to allocate Ycbcr texture directly.
DEF_GANESH_TEST_FOR_VULKAN_CONTEXT(VkYCbcrSampler_NoYcbcrSurface,
                                   reporter,
                                   ctxInfo,
                                   CtsEnforcement::kApiLevel_T) {
    GrDirectContext* dContext = ctxInfo.directContext();

    VkYcbcrSamplerHelper ycbcrHelper(dContext);
    if (!ycbcrHelper.isYCbCrSupported()) {
        return;
    }

    GrBackendTexture texture = dContext->createBackendTexture(
            kImageWidth,
            kImageHeight,
            GrBackendFormats::MakeVk(VK_FORMAT_G8_B8R8_2PLANE_420_UNORM),
            skgpu::Mipmapped::kNo,
            GrRenderable::kNo,
            GrProtected::kNo);
    if (texture.isValid()) {
        ERRORF(reporter,
               "GrDirectContext::createBackendTexture() didn't fail as expected for Ycbcr format.");
    }
}

#endif  // defined(SK_GANESH) && defined(SK_VULKAN)
