/*
 * Copyright 2023 Google LLC
 *
 * 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/codec/SkCodec.h"
#include "include/codec/SkEncodedImageFormat.h"
#include "include/codec/SkPngDecoder.h"
#include "include/core/SkAlphaType.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkImage.h"
#include "include/core/SkRect.h"
#include "include/core/SkSize.h"
#include "include/core/SkStream.h"
#include "include/core/SkString.h"
#include "include/private/base/SkTArray.h"
#include "include/private/base/SkTemplates.h"
#include "src/base/SkAutoMalloc.h"
#include "src/core/SkSwizzlePriv.h"
#include "src/utils/SkOSPath.h"
#include "tools/flags/CommandLineFlags.h"
#include "tools/flags/CommonFlags.h"

#include <map>
#include <memory>
#include <string>
#include <vector>

DEFINE_string(pngCodecGMImages,
              "",
              "Zero or more images or directories where to find PNG images to test with "
              "PNGCodecGM. Directories are scanned non-recursively. All files are assumed to be "
              "PNG images.");
DEFINE_string(pngCodecDecodeMode,
              "",
              "One of \"get-all-pixels\", \"incremental\" or \"zero-init\".");
DEFINE_string(pngCodecDstColorType,
              "",
              "One of \"force-grayscale\", "
              "\"force-nonnative-premul-color\" or \"get-from-canvas\".");
DEFINE_string(pngCodecDstAlphaType, "", "One of \"premul\" or \"unpremul\".");

static constexpr const char* sk_color_type_to_str(SkColorType colorType) {
    switch (colorType) {
        case kUnknown_SkColorType:
            return "kUnknown_SkColorType";
        case kAlpha_8_SkColorType:
            return "kAlpha_8_SkColorType";
        case kRGB_565_SkColorType:
            return "kRGB_565_SkColorType";
        case kARGB_4444_SkColorType:
            return "kARGB_4444_SkColorType";
        case kRGBA_8888_SkColorType:
            return "kRGBA_8888_SkColorType";
        case kRGB_888x_SkColorType:
            return "kRGB_888x_SkColorType";
        case kBGRA_8888_SkColorType:
            return "kBGRA_8888_SkColorType";
        case kRGBA_1010102_SkColorType:
            return "kRGBA_1010102_SkColorType";
        case kBGRA_1010102_SkColorType:
            return "kBGRA_1010102_SkColorType";
        case kRGB_101010x_SkColorType:
            return "kRGB_101010x_SkColorType";
        case kBGR_101010x_SkColorType:
            return "kBGR_101010x_SkColorType";
        case kBGR_101010x_XR_SkColorType:
            return "kBGR_101010x_XR_SkColorType";
        case kGray_8_SkColorType:
            return "kGray_8_SkColorType";
        case kRGBA_F16Norm_SkColorType:
            return "kRGBA_F16Norm_SkColorType";
        case kRGBA_F16_SkColorType:
            return "kRGBA_F16_SkColorType";
        case kRGB_F16F16F16x_SkColorType:
            return "kRGB_F16F16F16x_SkColorType";
        case kRGBA_F32_SkColorType:
            return "kRGBA_F32_SkColorType";
        case kR8G8_unorm_SkColorType:
            return "kR8G8_unorm_SkColorType";
        case kA16_float_SkColorType:
            return "kA16_float_SkColorType";
        case kR16G16_float_SkColorType:
            return "kR16G16_float_SkColorType";
        case kA16_unorm_SkColorType:
            return "kA16_unorm_SkColorType";
        case kR16_unorm_SkColorType:
            return "R16_unorm_SkColorType";
        case kR16G16_unorm_SkColorType:
            return "kR16G16_unorm_SkColorType";
        case kR16G16B16A16_unorm_SkColorType:
            return "kR16G16B16A16_unorm_SkColorType";
        case kSRGBA_8888_SkColorType:
            return "kSRGBA_8888_SkColorType";
        case kR8_unorm_SkColorType:
            return "kR8_unorm_SkColorType";
        case kRGBA_10x6_SkColorType:
            return "kRGBA_10x6_SkColorType";
        case kBGRA_10101010_XR_SkColorType:
            return "kBGRA_10101010_XR_SkColorType";
    }
    SkUNREACHABLE;
}

static constexpr const char* sk_alpha_type_to_str(SkAlphaType alphaType) {
    switch (alphaType) {
        case kUnknown_SkAlphaType:
            return "kUnknown_SkAlphaType";
        case kOpaque_SkAlphaType:
            return "kOpaque_SkAlphaType";
        case kPremul_SkAlphaType:
            return "kPremul_SkAlphaType";
        case kUnpremul_SkAlphaType:
            return "kUnpremul_SkAlphaType";
    }
    SkUNREACHABLE;
}

struct DecodeResult {
    std::unique_ptr<SkCodec> codec;
    std::string errorMsg;
};

static DecodeResult decode(std::string path) {
    sk_sp<SkData> encoded(SkData::MakeFromFileName(path.c_str()));
    if (!encoded) {
        return {.errorMsg = SkStringPrintf("Could not read \"%s\".", path.c_str()).c_str()};
    }
    SkCodec::Result result;
    std::unique_ptr<SkCodec> codec = SkPngDecoder::Decode(SkMemoryStream::Make(encoded), &result);
    if (result != SkCodec::Result::kSuccess) {
        return {.errorMsg = SkStringPrintf("Could not create codec for \"%s\": %s.",
                                           path.c_str(),
                                           SkCodec::ResultToString(result))
                                    .c_str()};
    }
    return {.codec = std::move(codec)};
}

// This GM implements the PNG-related behaviors found in DM's CodecSrc class. It takes a single
// image as an argument and applies the same logic as CodecSrc.
//
// See the CodecSrc class here:
// https://skia.googlesource.com/skia/+/ce49fc71bc7cc25244020cd3e64764a6d08e54fb/dm/DMSrcSink.h#158.
class PNGCodecGM : public skiagm::GM {
public:
    // Based on CodecSrc::Mode.
    // https://skia.googlesource.com/skia/+/ce49fc71bc7cc25244020cd3e64764a6d08e54fb/dm/DMSrcSink.h#160
    enum class DecodeMode {
        kGetAllPixels,
        kIncremental,
        kZeroInit,
    };

    // Based on CodecSrc::DstColorType.
    // https://skia.googlesource.com/skia/+/ce49fc71bc7cc25244020cd3e64764a6d08e54fb/dm/DMSrcSink.h#172
    enum class DstColorType {
        kForceGrayscale,
        kForceNonNativePremulColor,
        kGetFromCanvas,
    };

    static constexpr const char* DecodeModeToString(DecodeMode decodeMode) {
        switch (decodeMode) {
            case DecodeMode::kGetAllPixels:
                return "kGetAllPixels";
            case DecodeMode::kIncremental:
                return "kIncremental";
            case DecodeMode::kZeroInit:
                return "kZeroInit";
        }
        SkUNREACHABLE;
    }

    static constexpr const char* DstColorTypeToString(DstColorType dstColorType) {
        switch (dstColorType) {
            case DstColorType::kForceGrayscale:
                return "kForceGrayscale";
            case DstColorType::kForceNonNativePremulColor:
                return "kForceNonNativePremulColor";
            case DstColorType::kGetFromCanvas:
                return "kGetFromCanvas";
        }
        SkUNREACHABLE;
    }

    // Based on DM's CodecSrc::CodecSrc().
    // https://skia.googlesource.com/skia/+/ce49fc71bc7cc25244020cd3e64764a6d08e54fb/dm/DMSrcSink.cpp#371
    PNGCodecGM(std::string path,
               DecodeMode decodeMode,
               DstColorType dstColorType,
               SkAlphaType dstAlphaType)
            : skiagm::GM()
            , fPath(path)
            , fDecodeMode(decodeMode)
            , fDstColorType(dstColorType)
            , fDstAlphaType(dstAlphaType) {}

    bool isBazelOnly() const override {
        // This GM class overlaps with DM's CodecSrc and related sources.
        return true;
    }

    std::map<std::string, std::string> getGoldKeys() const override {
        return std::map<std::string, std::string>{
                {"name", getName().c_str()},
                {"source_type", "image"},
                {"decode_mode", DecodeModeToString(fDecodeMode)},
                {"dst_color_type", DstColorTypeToString(fDstColorType)},
                {"dst_alpha_type", sk_alpha_type_to_str(fDstAlphaType)},
        };
    }

protected:
    // Based on CodecSrc::name().
    // https://skia.googlesource.com/skia/+/ce49fc71bc7cc25244020cd3e64764a6d08e54fb/dm/DMSrcSink.cpp#828
    SkString getName() const override {
        SkString name = SkOSPath::Basename(fPath.c_str());
        return name;
    }

    // Based on CodecSrc::size().
    // https://skia.googlesource.com/skia/+/ce49fc71bc7cc25244020cd3e64764a6d08e54fb/dm/DMSrcSink.cpp#803
    SkISize getISize() override {
        DecodeResult decodeResult = decode(fPath);
        if (decodeResult.errorMsg != "") {
            return {0, 0};
        }
        return decodeResult.codec->dimensions();
    }

    // Based on CodecSrc::draw().
    // https://skia.googlesource.com/skia/+/ce49fc71bc7cc25244020cd3e64764a6d08e54fb/dm/DMSrcSink.cpp#450
    DrawResult onDraw(SkCanvas* canvas, SkString* errorMsg) override {
        DecodeResult decodeResult = decode(fPath);
        if (decodeResult.errorMsg != "") {
            *errorMsg = decodeResult.errorMsg.c_str();
            return DrawResult::kFail;
        }
        std::unique_ptr<SkCodec> codec = std::move(decodeResult.codec);

        SkImageInfo decodeInfo = codec->getInfo();
        if (*errorMsg = validateCanvasColorTypeAndGetDecodeInfo(&decodeInfo,
                                                                canvas->imageInfo().colorType());
            *errorMsg != SkString()) {
            return DrawResult::kFail;
        }

        SkISize size = codec->dimensions();
        decodeInfo = decodeInfo.makeDimensions(size);

        const int bpp = decodeInfo.bytesPerPixel();
        const size_t rowBytes = size.width() * bpp;
        const size_t safeSize = decodeInfo.computeByteSize(rowBytes);
        SkAutoMalloc pixels(safeSize);

        SkCodec::Options options;
        if (DecodeMode::kZeroInit == fDecodeMode) {
            memset(pixels.get(), 0, size.height() * rowBytes);
            options.fZeroInitialized = SkCodec::kYes_ZeroInitialized;
        }

        // For codec srcs, we want the "draw" step to be a memcpy.  Any interesting color space or
        // color format conversions should be performed by the codec.  Sometimes the output of the
        // decode will be in an interesting color space.  On our srgb and f16 backends, we need to
        // "pretend" that the color space is standard sRGB to avoid triggering color conversion
        // at draw time.
        SkImageInfo bitmapInfo = decodeInfo.makeColorSpace(SkColorSpace::MakeSRGB());

        if (kRGBA_8888_SkColorType == decodeInfo.colorType() ||
            kBGRA_8888_SkColorType == decodeInfo.colorType()) {
            bitmapInfo = bitmapInfo.makeColorType(kN32_SkColorType);
        }

        switch (fDecodeMode) {
            case DecodeMode::kZeroInit:
            case DecodeMode::kGetAllPixels: {
                switch (codec->getPixels(decodeInfo, pixels.get(), rowBytes, &options)) {
                    case SkCodec::kSuccess:
                        // We consider these to be valid, since we should still decode what is
                        // available.
                    case SkCodec::kErrorInInput:
                    case SkCodec::kIncompleteInput:
                        break;
                    default:
                        // Everything else is considered a failure.
                        *errorMsg = SkStringPrintf("Couldn't getPixels %s.", fPath.c_str());
                        return DrawResult::kFail;
                }

                drawToCanvas(canvas, bitmapInfo, pixels.get(), rowBytes);
                break;
            }
            case DecodeMode::kIncremental: {
                void* dst = pixels.get();
                uint32_t height = decodeInfo.height();
                if (SkCodec::kSuccess ==
                    codec->startIncrementalDecode(decodeInfo, dst, rowBytes, &options)) {
                    int rowsDecoded;
                    auto result = codec->incrementalDecode(&rowsDecoded);
                    if (SkCodec::kIncompleteInput == result || SkCodec::kErrorInInput == result) {
                        codec->fillIncompleteImage(decodeInfo,
                                                   dst,
                                                   rowBytes,
                                                   SkCodec::kNo_ZeroInitialized,
                                                   height,
                                                   rowsDecoded);
                    }
                } else {
                    *errorMsg = "Could not start incremental decode";
                    return DrawResult::kFail;
                }
                drawToCanvas(canvas, bitmapInfo, dst, rowBytes);
                break;
            }
            default:
                SkASSERT(false);
                *errorMsg = "Invalid fDecodeMode";
                return DrawResult::kFail;
        }
        return DrawResult::kOk;
    }

private:
    // Checks that the canvas color type, destination color and alpha types and input image
    // constitute an interesting test case, and constructs the SkImageInfo to use when decoding the
    // image.
    //
    // Based on DM's get_decode_info() function.
    // https://skia.googlesource.com/skia/+/ce49fc71bc7cc25244020cd3e64764a6d08e54fb/dm/DMSrcSink.cpp#398
    SkString validateCanvasColorTypeAndGetDecodeInfo(SkImageInfo* decodeInfo,
                                                     SkColorType canvasColorType) {
        switch (fDstColorType) {
            case DstColorType::kForceGrayscale:
                if (kRGB_565_SkColorType == canvasColorType) {
                    return SkStringPrintf(
                            "canvas color type %s and destination color type %s are redundant",
                            sk_color_type_to_str(canvasColorType),
                            DstColorTypeToString(fDstColorType));
                }
                *decodeInfo = decodeInfo->makeColorType(kGray_8_SkColorType);
                break;

            case DstColorType::kForceNonNativePremulColor:
                if (kRGB_565_SkColorType == canvasColorType ||
                    kRGBA_F16_SkColorType == canvasColorType) {
                    return SkStringPrintf(
                            "canvas color type %s and destination color type %s are redundant",
                            sk_color_type_to_str(canvasColorType),
                            DstColorTypeToString(fDstColorType));
                }
#ifdef SK_PMCOLOR_IS_RGBA
                *decodeInfo = decodeInfo->makeColorType(kBGRA_8888_SkColorType);
#else
                *decodeInfo = decodeInfo->makeColorType(kRGBA_8888_SkColorType);
#endif
                break;

            case DstColorType::kGetFromCanvas:
                if (kRGB_565_SkColorType == canvasColorType &&
                    kOpaque_SkAlphaType != decodeInfo->alphaType()) {
                    return SkStringPrintf(
                            "image \"%s\" has alpha type %s; this is incompatible with with "
                            "canvas color type %s and destination color type %s",
                            fPath.c_str(),
                            sk_alpha_type_to_str(decodeInfo->alphaType()),
                            sk_color_type_to_str(canvasColorType),
                            DstColorTypeToString(fDstColorType));
                }
                *decodeInfo = decodeInfo->makeColorType(canvasColorType);
                break;

            default:
                SkUNREACHABLE;
        }

        *decodeInfo = decodeInfo->makeAlphaType(fDstAlphaType);
        return SkString();
    }

    // Based on DM's draw_to_canvas() function.
    // https://skia.googlesource.com/skia/+/ce49fc71bc7cc25244020cd3e64764a6d08e54fb/dm/DMSrcSink.cpp#432
    void drawToCanvas(SkCanvas* canvas,
                      const SkImageInfo& info,
                      void* pixels,
                      size_t rowBytes,
                      SkScalar left = 0,
                      SkScalar top = 0) {
        SkBitmap bitmap;
        bitmap.installPixels(info, pixels, rowBytes);
        swapRbIfNecessary(bitmap);
        canvas->drawImage(bitmap.asImage(), left, top);
    }

    // Allows us to test decodes to non-native 8888.
    //
    // Based on DM's swap_rb_if_necessary function.
    // https://skia.googlesource.com/skia/+/ce49fc71bc7cc25244020cd3e64764a6d08e54fb/dm/DMSrcSink.cpp#387
    void swapRbIfNecessary(SkBitmap& bitmap) {
        if (DstColorType::kForceNonNativePremulColor != fDstColorType) {
            return;
        }

        for (int y = 0; y < bitmap.height(); y++) {
            uint32_t* row = (uint32_t*)bitmap.getAddr(0, y);
            SkOpts::RGBA_to_BGRA(row, row, bitmap.width());
        }
    }

    std::string fPath;
    DecodeMode fDecodeMode;
    DstColorType fDstColorType;
    SkAlphaType fDstAlphaType;
};

// Registers GMs with zero or more PNGCodecGM instances for the given image. Returns a non-empty,
// human-friendly error message in the case of errors.
//
// Based on DM's push_codec_srcs() function. It only covers "simple" codecs (lines 740-834).
// https://skia.googlesource.com/skia/+/ce49fc71bc7cc25244020cd3e64764a6d08e54fb/dm/DM.cpp#740
//
// Specifically, this function does not capture any behaviors found in the following DM classes:
//
//  - AndroidCodecSrc
//  - BRDSrc
//  - ImageGenSrc
//
// TODO(lovisolo): Implement the above sources as GMs (if necessary).
static std::string registerGMsForImage(std::string path,
                                       PNGCodecGM::DecodeMode decodeMode,
                                       PNGCodecGM::DstColorType dstColorType,
                                       SkAlphaType dstAlphaType) {
    DecodeResult decodeResult = decode(path);
    if (decodeResult.errorMsg != "") {
        return decodeResult.errorMsg;
    }

    if (dstColorType == PNGCodecGM::DstColorType::kForceGrayscale &&
        decodeResult.codec->getInfo().colorType() != kGray_8_SkColorType) {
        return SkStringPrintf(
                       "image \"%s\" has color type %s; this is incompatible with the given "
                       "dstColorType argument: %s (expected image color type: %s)",
                       path.c_str(),
                       sk_color_type_to_str(decodeResult.codec->getInfo().colorType()),
                       PNGCodecGM::DstColorTypeToString(PNGCodecGM::DstColorType::kForceGrayscale),
                       sk_color_type_to_str(kGray_8_SkColorType))
                .c_str();
    }

    if (dstAlphaType == kUnpremul_SkAlphaType &&
        decodeResult.codec->getInfo().alphaType() == kOpaque_SkAlphaType) {
        return SkStringPrintf(
                       "image \"%s\" has alpha type %s; this is incompatible with the given "
                       "dstAlphaType argument: %s",
                       path.c_str(),
                       sk_alpha_type_to_str(kOpaque_SkAlphaType),
                       sk_alpha_type_to_str(kUnpremul_SkAlphaType))
                .c_str();
    }

    skiagm::Register(new PNGCodecGM(path, decodeMode, dstColorType, dstAlphaType));
    return "";
}

// Returns a non-empty message in the case of errors.
static std::string parse_and_validate_flags(PNGCodecGM::DecodeMode* decodeMode,
                                            PNGCodecGM::DstColorType* dstColorType,
                                            SkAlphaType* dstAlphaType) {
    skia_private::THashMap<SkString, PNGCodecGM::DecodeMode> decodeModeValues = {
            {SkString("get-all-pixels"), PNGCodecGM::DecodeMode::kGetAllPixels},
            {SkString("incremental"), PNGCodecGM::DecodeMode::kIncremental},
            {SkString("zero-init"), PNGCodecGM::DecodeMode::kZeroInit},
    };
    if (SkString errorMsg = FLAGS_pngCodecDecodeMode.parseAndValidate(
                "--pngCodecDecodeMode", decodeModeValues, decodeMode);
        errorMsg != SkString()) {
        return errorMsg.c_str();
    }

    skia_private::THashMap<SkString, PNGCodecGM::DstColorType> dstColorTypeValues = {
            {SkString("get-from-canvas"), PNGCodecGM::DstColorType::kGetFromCanvas},
            {SkString("force-grayscale"), PNGCodecGM::DstColorType::kForceGrayscale},
            {SkString("force-nonnative-premul-color"),
             PNGCodecGM::DstColorType::kForceNonNativePremulColor},
    };
    if (SkString errorMsg = FLAGS_pngCodecDstColorType.parseAndValidate(
                "--pngCodecDstColorType", dstColorTypeValues, dstColorType);
        errorMsg != SkString()) {
        return errorMsg.c_str();
    }

    skia_private::THashMap<SkString, SkAlphaType> dstAlphaTypeValues = {
            {SkString("premul"), kPremul_SkAlphaType},
            {SkString("unpremul"), kUnpremul_SkAlphaType},
    };
    if (SkString errorMsg = FLAGS_pngCodecDstAlphaType.parseAndValidate(
                "--pngCodecDstAlphaType", dstAlphaTypeValues, dstAlphaType);
        errorMsg != SkString()) {
        return errorMsg.c_str();
    }

    return "";
}

// Registers one PNGCodecGM instance for each image passed via the --pngCodecGMImages flag, which
// can take files and directories. Directories are scanned non-recursively.
//
// Based on DM's gather_srcs() function.
// https://skia.googlesource.com/skia/+/ce49fc71bc7cc25244020cd3e64764a6d08e54fb/dm/DM.cpp#953
DEF_GM_REGISTERER_FN([]() -> std::string {
    // Parse flags.
    PNGCodecGM::DecodeMode decodeMode;
    PNGCodecGM::DstColorType dstColorType;
    SkAlphaType dstAlphaType;
    if (std::string errorMsg = parse_and_validate_flags(&decodeMode, &dstColorType, &dstAlphaType);
        errorMsg != "") {
        return errorMsg;
    }

    // Collect images.
    skia_private::TArray<SkString> images;
    if (!CommonFlags::CollectImages(FLAGS_pngCodecGMImages, &images)) {
        return "Failed to collect images.";
    }

    // Register one GM per image.
    for (const SkString& image : images) {
        if (std::string errorMsg =
                    registerGMsForImage(image.c_str(), decodeMode, dstColorType, dstAlphaType);
            errorMsg != "") {
            return errorMsg;
        }
    }

    return "";
});
