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

#include "gm/gm.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkImage.h"
#include "include/core/SkStream.h"
#include "include/core/SkTextureCompressionType.h"
#include "include/gpu/GrDirectContext.h"
#include "include/gpu/GrRecordingContext.h"
#include "src/core/SkCompressedDataUtils.h"
#include "src/core/SkMipmap.h"
#include "src/gpu/ganesh/GrCaps.h"
#include "src/gpu/ganesh/GrImageContextPriv.h"
#include "src/gpu/ganesh/GrRecordingContextPriv.h"
#include "src/gpu/ganesh/gl/GrGLDefines_impl.h"
#include "src/image/SkImage_Base.h"
#include "src/image/SkImage_GpuBase.h"
#include "tools/Resources.h"
#include "tools/gpu/ProxyUtils.h"

using namespace skia_private;

//-------------------------------------------------------------------------------------------------
struct ImageInfo {
    SkISize           fDim;
    GrMipmapped       fMipmapped;
    SkTextureCompressionType fCompressionType;
};

/*
 * Get an int from a buffer
 * This method is unsafe, the caller is responsible for performing a check
 */
static inline uint32_t get_uint(uint8_t* buffer, uint32_t i) {
    uint32_t result;
    memcpy(&result, &(buffer[i]), 4);
    return result;
}

// This KTX loader is barely sufficient to load the specific files this GM requires. Use
// at your own peril.
static sk_sp<SkData> load_ktx(const char* filename, ImageInfo* imageInfo) {
    SkFILEStream input(filename);
    if (!input.isValid()) {
        return nullptr;
    }

    constexpr int kKTXIdentifierSize = 12;
    constexpr int kKTXHeaderSize = kKTXIdentifierSize + 13 * sizeof(uint32_t);
    uint8_t header[kKTXHeaderSize];

    if (input.read(header, kKTXHeaderSize) != kKTXHeaderSize) {
        return nullptr;
    }

    static const uint8_t kExpectedIdentifier[kKTXIdentifierSize] = {
        0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A
    };

    if (0 != memcmp(header, kExpectedIdentifier, kKTXIdentifierSize)) {
        return nullptr;
    }

    uint32_t endianness = get_uint(header, 12);
    if (endianness != 0x04030201) {
        // TODO: need to swap rest of header and, if glTypeSize is > 1, all
        // the texture data.
        return nullptr;
    }

    uint32_t glType = get_uint(header, 16);
    SkDEBUGCODE(uint32_t glTypeSize = get_uint(header, 20);)
    uint32_t glFormat = get_uint(header, 24);
    uint32_t glInternalFormat = get_uint(header, 28);
    //uint32_t glBaseInternalFormat = get_uint(header, 32);
    uint32_t pixelWidth = get_uint(header, 36);
    uint32_t pixelHeight = get_uint(header, 40);
    uint32_t pixelDepth = get_uint(header, 44);
    //uint32_t numberOfArrayElements = get_uint(header, 48);
    uint32_t numberOfFaces = get_uint(header, 52);
    int numberOfMipmapLevels = get_uint(header, 56);
    uint32_t bytesOfKeyValueData = get_uint(header, 60);

    if (glType != 0 || glFormat != 0) {  // only care about compressed data for now
        return nullptr;
    }
    SkASSERT(glTypeSize == 1); // required for compressed data

    // We only handle these four formats right now
    switch (glInternalFormat) {
        case GR_GL_COMPRESSED_ETC1_RGB8:
        case GR_GL_COMPRESSED_RGB8_ETC2:
            imageInfo->fCompressionType = SkTextureCompressionType::kETC2_RGB8_UNORM;
            break;
        case GR_GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
            imageInfo->fCompressionType = SkTextureCompressionType::kBC1_RGB8_UNORM;
            break;
        case GR_GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
            imageInfo->fCompressionType = SkTextureCompressionType::kBC1_RGBA8_UNORM;
            break;
        default:
            return nullptr;
    }

    imageInfo->fDim.fWidth = pixelWidth;
    imageInfo->fDim.fHeight = pixelHeight;

    if (pixelDepth != 0) {
        return nullptr; // pixel depth is always zero for 2D textures
    }

    if (numberOfFaces != 1) {
        return nullptr; // we don't support cube maps right now
    }

    if (numberOfMipmapLevels == 1) {
        imageInfo->fMipmapped = GrMipmapped::kNo;
    } else {
        int numRequiredMipLevels = SkMipmap::ComputeLevelCount(pixelWidth, pixelHeight)+1;
        if (numberOfMipmapLevels != numRequiredMipLevels) {
            return nullptr;
        }
        imageInfo->fMipmapped = GrMipmapped::kYes;
    }

    if (bytesOfKeyValueData != 0) {
        return nullptr;
    }

    TArray<size_t> individualMipOffsets(numberOfMipmapLevels);

    size_t dataSize = SkCompressedDataSize(imageInfo->fCompressionType,
                                           { (int) pixelWidth, (int) pixelHeight },
                                           &individualMipOffsets,
                                           imageInfo->fMipmapped == GrMipmapped::kYes);
    SkASSERT(individualMipOffsets.size() == numberOfMipmapLevels);

    sk_sp<SkData> data = SkData::MakeUninitialized(dataSize);

    uint8_t* dest = (uint8_t*) data->writable_data();

    size_t offset = 0;
    for (int i = 0; i < numberOfMipmapLevels; ++i) {
        uint32_t imageSize;

        if (input.read(&imageSize, 4) != 4) {
            return nullptr;
        }

        SkASSERT(offset + imageSize <= dataSize);
        SkASSERT(offset == individualMipOffsets[i]);

        if (input.read(&dest[offset], imageSize) != imageSize) {
            return nullptr;
        }

        offset += imageSize;
    }

    return data;
}

//-------------------------------------------------------------------------------------------------
typedef uint32_t DWORD;

// Values for the DDS_PIXELFORMAT 'dwFlags' field
constexpr unsigned int kDDPF_FOURCC      = 0x4;

struct DDS_PIXELFORMAT {
    DWORD dwSize;
    DWORD dwFlags;
    DWORD dwFourCC;
    DWORD dwRGBBitCount;
    DWORD dwRBitMask;
    DWORD dwGBitMask;
    DWORD dwBBitMask;
    DWORD dwABitMask;
};

// Values for the DDS_HEADER 'dwFlags' field
constexpr unsigned int kDDSD_CAPS        = 0x1;        // required
constexpr unsigned int kDDSD_HEIGHT      = 0x2;        // required
constexpr unsigned int kDDSD_WIDTH       = 0x4;        // required
constexpr unsigned int kDDSD_PITCH       = 0x8;
constexpr unsigned int kDDSD_PIXELFORMAT = 0x001000;   // required
constexpr unsigned int kDDSD_MIPMAPCOUNT = 0x020000;
constexpr unsigned int kDDSD_LINEARSIZE  = 0x080000;
constexpr unsigned int kDDSD_DEPTH       = 0x800000;

constexpr unsigned int kDDSD_REQUIRED = kDDSD_CAPS | kDDSD_HEIGHT | kDDSD_WIDTH | kDDSD_PIXELFORMAT;

typedef struct {
    DWORD           dwSize;
    DWORD           dwFlags;
    DWORD           dwHeight;
    DWORD           dwWidth;
    DWORD           dwPitchOrLinearSize;
    DWORD           dwDepth;
    DWORD           dwMipMapCount;
    DWORD           dwReserved1[11];
    DDS_PIXELFORMAT ddspf;
    DWORD           dwCaps;
    DWORD           dwCaps2;
    DWORD           dwCaps3;
    DWORD           dwCaps4;
    DWORD           dwReserved2;
} DDS_HEADER;

// This DDS loader is barely sufficient to load the specific files this GM requires. Use
// at your own peril.
static sk_sp<SkData> load_dds(const char* filename, ImageInfo* imageInfo) {
    SkFILEStream input(filename);
    if (!input.isValid()) {
        return nullptr;
    }

    constexpr uint32_t kMagic = 0x20534444;
    uint32_t magic;

    if (input.read(&magic, 4) != 4) {
        return nullptr;
    }

    if (magic != kMagic) {
        return nullptr;
    }

    constexpr size_t kDDSHeaderSize = sizeof(DDS_HEADER);
    static_assert(kDDSHeaderSize == 124);
    constexpr size_t kDDSPixelFormatSize = sizeof(DDS_PIXELFORMAT);
    static_assert(kDDSPixelFormatSize == 32);

    DDS_HEADER header;

    if (input.read(&header, kDDSHeaderSize) != kDDSHeaderSize) {
        return nullptr;
    }

    if (header.dwSize != kDDSHeaderSize ||
        header.ddspf.dwSize != kDDSPixelFormatSize) {
        return nullptr;
    }

    if ((header.dwFlags & kDDSD_REQUIRED) != kDDSD_REQUIRED) {
        return nullptr;
    }

    if (header.dwFlags & (kDDSD_PITCH | kDDSD_LINEARSIZE | kDDSD_DEPTH)) {
        // TODO: support these features
    }

    imageInfo->fDim.fWidth = header.dwWidth;
    imageInfo->fDim.fHeight = header.dwHeight;

    int numberOfMipmapLevels = 1;
    if (header.dwFlags & kDDSD_MIPMAPCOUNT) {
        if (header.dwMipMapCount == 1) {
            imageInfo->fMipmapped = GrMipmapped::kNo;
        } else {
            int numRequiredLevels = SkMipmap::ComputeLevelCount(header.dwWidth, header.dwHeight)+1;
            if (header.dwMipMapCount != (unsigned) numRequiredLevels) {
                return nullptr;
            }
            imageInfo->fMipmapped = GrMipmapped::kYes;
            numberOfMipmapLevels = numRequiredLevels;
        }
    } else {
        imageInfo->fMipmapped = GrMipmapped::kNo;
    }

    if (!(header.ddspf.dwFlags & kDDPF_FOURCC)) {
        return nullptr;
    }

    // We only handle these one format right now
    switch (header.ddspf.dwFourCC) {
        case 0x31545844: // DXT1
            imageInfo->fCompressionType = SkTextureCompressionType::kBC1_RGB8_UNORM;
            break;
        default:
            return nullptr;
    }

    TArray<size_t> individualMipOffsets(numberOfMipmapLevels);

    size_t dataSize = SkCompressedDataSize(imageInfo->fCompressionType,
                                           { (int) header.dwWidth, (int) header.dwHeight },
                                           &individualMipOffsets,
                                           imageInfo->fMipmapped == GrMipmapped::kYes);
    SkASSERT(individualMipOffsets.size() == numberOfMipmapLevels);

    sk_sp<SkData> data = SkData::MakeUninitialized(dataSize);

    uint8_t* dest = (uint8_t*) data->writable_data();

    size_t amountRead = input.read(dest, dataSize);
    if (amountRead != dataSize) {
        return nullptr;
    }

    return data;
}

//-------------------------------------------------------------------------------------------------
static sk_sp<SkImage> data_to_img(GrDirectContext *direct, sk_sp<SkData> data,
                                  const ImageInfo& info) {
    if (direct) {
        return SkImage::MakeTextureFromCompressed(direct, std::move(data),
                                                  info.fDim.fWidth,
                                                  info.fDim.fHeight,
                                                  info.fCompressionType,
                                                  info.fMipmapped);
    } else {
        return SkImage::MakeRasterFromCompressed(std::move(data),
                                                 info.fDim.fWidth,
                                                 info.fDim.fHeight,
                                                 info.fCompressionType);
    }
}

namespace skiagm {

// This GM exercises our handling of some of the more exotic formats using externally
// generated content. Right now it only tests ETC1 and BC1.
class ExoticFormatsGM : public GM {
public:
    ExoticFormatsGM() {
        this->setBGColor(SK_ColorBLACK);
    }

protected:
    SkString onShortName() override {
        return SkString("exoticformats");
    }

    SkISize onISize() override {
        return SkISize::Make(2*kImgWidthHeight + 3 * kPad, kImgWidthHeight + 2 * kPad);
    }

    bool loadImages(GrDirectContext *direct) {
        SkASSERT(!fETC1Image && !fBC1Image);

        {
            ImageInfo info;
            sk_sp<SkData> data = load_ktx(GetResourcePath("images/flower-etc1.ktx").c_str(), &info);
            if (data) {
                SkASSERT(info.fDim.equals(kImgWidthHeight, kImgWidthHeight));
                SkASSERT(info.fMipmapped == GrMipmapped::kNo);
                SkASSERT(info.fCompressionType == SkTextureCompressionType::kETC2_RGB8_UNORM);

                fETC1Image = data_to_img(direct, std::move(data), info);
            } else {
                SkDebugf("failed to load flower-etc1.ktx\n");
                return false;
            }
        }

        {
            ImageInfo info;
            sk_sp<SkData> data = load_dds(GetResourcePath("images/flower-bc1.dds").c_str(), &info);
            if (data) {
                SkASSERT(info.fDim.equals(kImgWidthHeight, kImgWidthHeight));
                SkASSERT(info.fMipmapped == GrMipmapped::kNo);
                SkASSERT(info.fCompressionType == SkTextureCompressionType::kBC1_RGB8_UNORM);

                fBC1Image = data_to_img(direct, std::move(data), info);
            } else {
                SkDebugf("failed to load flower-bc1.dds\n");
                return false;
            }
        }

        return true;
    }

    void drawImage(SkCanvas* canvas, SkImage* image, int x, int y) {
        if (!image) {
            return;
        }

        bool isCompressed = false;
        if (image->isTextureBacked()) {
            const GrCaps* caps = as_IB(image)->context()->priv().caps();
            GrTextureProxy* proxy = sk_gpu_test::GetTextureImageProxy(image,
                                                                      canvas->recordingContext());
            isCompressed = caps->isFormatCompressed(proxy->backendFormat());
        }

        canvas->drawImage(image, x, y);

        if (!isCompressed) {
            // Make it obvious which drawImages used decompressed images
            SkRect r = SkRect::MakeXYWH(x, y, kImgWidthHeight, kImgWidthHeight);
            SkPaint paint;
            paint.setColor(SK_ColorRED);
            paint.setStyle(SkPaint::kStroke_Style);
            paint.setStrokeWidth(2.0f);
            canvas->drawRect(r, paint);
        }
    }

    DrawResult onGpuSetup(SkCanvas* canvas, SkString* errorMsg) override {
        auto dContext = GrAsDirectContext(canvas->recordingContext());
        if (dContext && dContext->abandoned()) {
            // This isn't a GpuGM so a null 'context' is okay but an abandoned context
            // if forbidden.
            return DrawResult::kSkip;
        }

        if (!this->loadImages(dContext)) {
            *errorMsg = "Failed to create images.";
            return DrawResult::kFail;
        }

        return DrawResult::kOk;
    }

    void onGpuTeardown() override {
        fETC1Image = nullptr;
        fBC1Image = nullptr;
    }

    void onDraw(SkCanvas* canvas) override {
        SkASSERT(fETC1Image && fBC1Image);

        this->drawImage(canvas, fETC1Image.get(), kPad, kPad);
        this->drawImage(canvas, fBC1Image.get(), kImgWidthHeight + 2 * kPad, kPad);
    }

private:
    static const int kImgWidthHeight = 128;
    static const int kPad = 4;

    sk_sp<SkImage> fETC1Image;
    sk_sp<SkImage> fBC1Image;

    using INHERITED = GM;
};

//////////////////////////////////////////////////////////////////////////////

DEF_GM(return new ExoticFormatsGM;)
}  // namespace skiagm
