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

#include "src/codec/SkPngCodecBase.h"

#include <algorithm>
#include <cstddef>
#include <tuple>
#include <utility>

#include "include/codec/SkCodec.h"
#include "include/codec/SkEncodedImageFormat.h"
#include "include/core/SkAlphaType.h"
#include "include/core/SkColor.h"
#include "include/core/SkColorType.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkRect.h"
#include "include/core/SkStream.h"
#include "include/private/SkEncodedInfo.h"
#include "include/private/base/SkAssert.h"
#include "include/private/base/SkSpan_impl.h"
#include "modules/skcms/skcms.h"
#include "src/codec/SkCodecPriv.h"
#include "src/codec/SkColorPalette.h"
#include "src/codec/SkSwizzler.h"
#include "src/core/SkMemset.h"
#include "src/core/SkSwizzlePriv.h"

namespace {

constexpr SkColorType kXformSrcColorType = kRGBA_8888_SkColorType;

inline bool needs_premul(SkAlphaType dstAT, SkEncodedInfo::Alpha encodedAlpha) {
    return kPremul_SkAlphaType == dstAT && SkEncodedInfo::kUnpremul_Alpha == encodedAlpha;
}

skcms_PixelFormat ToPixelFormat(const SkEncodedInfo& info) {
    // We use kRGB and kRGBA formats because color PNGs are always RGB or RGBA.
    if (16 == info.bitsPerComponent()) {
        if (SkEncodedInfo::kRGBA_Color == info.color()) {
            return skcms_PixelFormat_RGBA_16161616BE;
        } else if (SkEncodedInfo::kRGB_Color == info.color()) {
            return skcms_PixelFormat_RGB_161616BE;
        }
    } else if (SkEncodedInfo::kGray_Color == info.color()) {
        return skcms_PixelFormat_G_8;
    }

    return skcms_PixelFormat_RGBA_8888;
}

}  // namespace

SkPngCodecBase::~SkPngCodecBase() = default;

// static
bool SkPngCodecBase::isCompatibleColorProfileAndType(const SkEncodedInfo::ICCProfile* profile,
                                                     SkEncodedInfo::Color color) {
    if (profile) {
        switch (profile->profile()->data_color_space) {
            case skcms_Signature_CMYK:
                return false;
            case skcms_Signature_Gray:
                if (SkEncodedInfo::kGray_Color != color &&
                    SkEncodedInfo::kGrayAlpha_Color != color) {
                    return false;
                }
                break;
            default:
                break;
        }
    }

    return true;
}

SkPngCodecBase::SkPngCodecBase(SkEncodedInfo&& encodedInfo,
                               std::unique_ptr<SkStream> stream,
                               SkEncodedOrigin origin)
        : SkCodec(std::move(encodedInfo), ToPixelFormat(encodedInfo), std::move(stream), origin) {}

SkEncodedImageFormat SkPngCodecBase::onGetEncodedFormat() const {
    return SkEncodedImageFormat::kPNG;
}

SkCodec::Result SkPngCodecBase::initializeXforms(const SkImageInfo& dstInfo,
                                                 const Options& options,
                                                 int frameWidth) {
    if (frameWidth != dstInfo.width() && options.fSubset) {
        return kInvalidParameters;
    }
    fXformWidth = frameWidth;

    {
        size_t encodedBitsPerPixel = static_cast<size_t>(getEncodedInfo().bitsPerPixel());

        // We assume that `frameWidth` and `bitsPerPixel` have been already sanitized
        // earlier (and that the multiplication and addition below won't overflow).
        SkASSERT_RELEASE(0 < frameWidth);
        SkASSERT_RELEASE(frameWidth < 0xFFFFFF);
        SkASSERT_RELEASE(encodedBitsPerPixel < 128);

        size_t encodedBitsPerRow = static_cast<size_t>(frameWidth) * encodedBitsPerPixel;
        fEncodedRowBytes = (encodedBitsPerRow + 7) / 8;  // Round up to the next byte.

        size_t dstBytesPerPixel = dstInfo.bytesPerPixel();
        fDstRowBytes = static_cast<size_t>(frameWidth) * dstBytesPerPixel;
    }

    // Reset fSwizzler and this->colorXform().  We can't do this in onRewind() because the
    // interlaced scanline decoder may need to rewind.
    fSwizzler.reset(nullptr);

    // If skcms directly supports the encoded PNG format, we should skip format
    // conversion in the swizzler (or skip swizzling altogether).
    bool skipFormatConversion = false;
    switch (this->getEncodedInfo().color()) {
        case SkEncodedInfo::kRGB_Color:
            if (this->getEncodedInfo().bitsPerComponent() != 16) {
                break;
            }
            [[fallthrough]];
        case SkEncodedInfo::kRGBA_Color:
        case SkEncodedInfo::kGray_Color:
            skipFormatConversion = this->colorXform();
            break;
        default:
            break;
    }

    if (skipFormatConversion && !options.fSubset) {
        fXformMode = kColorOnly_XformMode;
    } else {
        if (SkEncodedInfo::kPalette_Color == this->getEncodedInfo().color()) {
            if (!this->createColorTable(dstInfo)) {
                return kInvalidInput;
            }
        }

        Result result =
                this->initializeSwizzler(dstInfo, options, skipFormatConversion, frameWidth);
        if (result != kSuccess) {
            return result;
        }
    }

    this->allocateStorage(dstInfo);

    // We can't call `initializeXformParams` here, because `swizzleWidth` may
    // change *after* `onStartIncrementalDecode`
    // (`SkSampledCodec::sampledDecode` first [transitively] calls
    // `onStartIncrementalDecode` and *then* `SkSwizzler::onSetSampleX`).

    return kSuccess;
}

void SkPngCodecBase::initializeXformParams() {
    if (fXformMode == kSwizzleColor_XformMode) {
        fXformWidth = this->swizzler()->swizzleWidth();
    }
}

void SkPngCodecBase::allocateStorage(const SkImageInfo& dstInfo) {
    switch (fXformMode) {
        case kSwizzleOnly_XformMode:
            break;
        case kColorOnly_XformMode:
            // Intentional fall through.  A swizzler hasn't been created yet, but one will
            // be created later if we are sampling.  We'll go ahead and allocate
            // enough memory to swizzle if necessary.
        case kSwizzleColor_XformMode: {
            const int bitsPerPixel = this->getEncodedInfo().bitsPerPixel();

            // If we have more than 8-bits (per component) of precision, we will keep that
            // extra precision.  Otherwise, we will swizzle to RGBA_8888 before transforming.
            const size_t bytesPerPixel = (bitsPerPixel > 32) ? bitsPerPixel / 8 : 4;
            const size_t colorXformBytes = dstInfo.width() * bytesPerPixel;
            fStorage.reset(colorXformBytes);
            break;
        }
    }
}

SkCodec::Result SkPngCodecBase::initializeSwizzler(const SkImageInfo& dstInfo,
                                                   const Options& options,
                                                   bool skipFormatConversion,
                                                   int frameWidth) {
    SkImageInfo swizzlerInfo = dstInfo;
    Options swizzlerOptions = options;
    fXformMode = kSwizzleOnly_XformMode;
    if (this->colorXform() && this->xformOnDecode()) {
        if (SkEncodedInfo::kGray_Color == this->getEncodedInfo().color()) {
            swizzlerInfo = swizzlerInfo.makeColorType(kGray_8_SkColorType);
        } else {
            swizzlerInfo = swizzlerInfo.makeColorType(kXformSrcColorType);
        }
        if (kPremul_SkAlphaType == dstInfo.alphaType()) {
            swizzlerInfo = swizzlerInfo.makeAlphaType(kUnpremul_SkAlphaType);
        }

        fXformMode = kSwizzleColor_XformMode;

        // Here, we swizzle into temporary memory, which is not zero initialized.
        // FIXME (msarett):
        // Is this a problem?
        swizzlerOptions.fZeroInitialized = kNo_ZeroInitialized;
    }

    SkIRect frameRect = SkIRect::MakeWH(frameWidth, 1);
    const SkIRect* frameRectPtr = nullptr;
    if (options.fSubset) {
        SkASSERT_RELEASE(frameWidth == dstInfo.width());
    } else {
        frameRectPtr = &frameRect;
    }

    if (skipFormatConversion) {
        // We cannot skip format conversion when there is a color table.
        SkASSERT_RELEASE(!fColorTable);
        int srcBPP = 0;
        switch (this->getEncodedInfo().color()) {
            case SkEncodedInfo::kRGB_Color:
                SkASSERT_RELEASE(this->getEncodedInfo().bitsPerComponent() == 16);
                srcBPP = 6;
                break;
            case SkEncodedInfo::kRGBA_Color:
                srcBPP = this->getEncodedInfo().bitsPerComponent() / 2;
                break;
            case SkEncodedInfo::kGray_Color:
                srcBPP = 1;
                break;
            default:
                SkASSERT_RELEASE(false);
                break;
        }
        fSwizzler = SkSwizzler::MakeSimple(srcBPP, swizzlerInfo, swizzlerOptions, frameRectPtr);
    } else {
        const SkPMColor* colors = SkCodecPriv::GetColorPtr(fColorTable.get());
        fSwizzler = SkSwizzler::Make(
                this->getEncodedInfo(), colors, swizzlerInfo, swizzlerOptions, frameRectPtr);
    }

    return !!fSwizzler ? kSuccess : kUnimplemented;
}

SkSampler* SkPngCodecBase::getSampler(bool createIfNecessary) {
    if (fSwizzler || !createIfNecessary) {
        return fSwizzler.get();
    }

    // Ok to ignore `initializeSwizzler`'s result, because if it fails, then
    // `fSwizzler` will be `nullptr` and we want to return `nullptr` upon
    // failure.
    std::ignore = this->initializeSwizzler(
            this->dstInfo(), this->options(), true, this->dstInfo().width());

    return fSwizzler.get();
}

void SkPngCodecBase::applyXformRow(SkSpan<uint8_t> dstRow, SkSpan<const uint8_t> srcRow) {
    SkASSERT_RELEASE(dstRow.size() >= fDstRowBytes);
    SkASSERT_RELEASE(srcRow.size() >= fEncodedRowBytes);
    applyXformRow(dstRow.data(), srcRow.data());
}

void SkPngCodecBase::applyXformRow(void* dstRow, const uint8_t* srcRow) {
    switch (fXformMode) {
        case kSwizzleOnly_XformMode:
            fSwizzler->swizzle(dstRow, srcRow);
            break;
        case kColorOnly_XformMode:
            this->applyColorXform(dstRow, srcRow, fXformWidth);
            break;
        case kSwizzleColor_XformMode:
            fSwizzler->swizzle(fStorage.get(), srcRow);
            this->applyColorXform(dstRow, fStorage.get(), fXformWidth);
            break;
    }
}

// Note: SkColorPalette claims to store SkPMColors, which is not necessarily the case here.
bool SkPngCodecBase::createColorTable(const SkImageInfo& dstInfo) {
    if (fDstInfoOfPreviousColorTableCreation.has_value() &&
        *fDstInfoOfPreviousColorTableCreation == dstInfo) {
        return !!fColorTable;
    }
    fColorTable.reset();
    fDstInfoOfPreviousColorTableCreation = dstInfo;

    std::optional<SkSpan<const PaletteColorEntry>> maybePlteChunk = this->onTryGetPlteChunk();
    if (!maybePlteChunk.has_value()) {
        return false;
    }
    const PaletteColorEntry* palette = maybePlteChunk->data();
    constexpr size_t kMaxCountOfPaletteEntries = 256;
    size_t numColors = maybePlteChunk->size();
    numColors = std::min(numColors, kMaxCountOfPaletteEntries);

    // Contents depend on tableColorType and our choice of if/when to premultiply:
    // { kPremul, kUnpremul, kOpaque } x { RGBA, BGRA }
    SkPMColor colorTable[kMaxCountOfPaletteEntries];
    SkColorType tableColorType = this->colorXform() ? kXformSrcColorType : dstInfo.colorType();

    std::optional<SkSpan<const uint8_t>> maybeTrnsChunk = this->onTryGetTrnsChunk();
    const uint8_t* alphas = nullptr;
    size_t numColorsWithAlpha = 0;
    if (maybeTrnsChunk.has_value()) {
        alphas = maybeTrnsChunk->data();
        numColorsWithAlpha = maybeTrnsChunk->size();
        numColorsWithAlpha = std::min(numColorsWithAlpha, numColors);
    }

    bool shouldApplyColorXformToColorTable = this->colorXform() && !this->xformOnDecode();
    if (alphas) {
        bool premultiply = !shouldApplyColorXformToColorTable &&
                           needs_premul(dstInfo.alphaType(), this->getEncodedInfo().alpha());

        // Choose which function to use to create the color table. If the final destination's
        // colortype is unpremultiplied, the color table will store unpremultiplied colors.
        SkCodecPriv::PackColorProc proc =
                SkCodecPriv::ChoosePackColorProc(premultiply, tableColorType);

        for (size_t i = 0; i < numColorsWithAlpha; i++) {
            // We don't have a function in SkOpts that combines a set of alphas with a set
            // of RGBs.  We could write one, but it's hardly worth it, given that this
            // is such a small fraction of the total decode time.
            colorTable[i] = proc(alphas[i], palette->red, palette->green, palette->blue);
            palette++;
        }
    }

    if (numColorsWithAlpha < numColors) {
        // The optimized code depends on a 3-byte png_color struct with the colors
        // in RGB order.  These checks make sure it is safe to use.
        static_assert(3 == sizeof(PaletteColorEntry));
        static_assert(offsetof(PaletteColorEntry, red) == 0);
        static_assert(offsetof(PaletteColorEntry, green) == 1);
        static_assert(offsetof(PaletteColorEntry, blue) == 2);

        if (SkCodecPriv::IsRGBA(tableColorType)) {
            SkOpts::RGB_to_RGB1(colorTable + numColorsWithAlpha,
                                (const uint8_t*)palette,
                                numColors - numColorsWithAlpha);
        } else {
            SkOpts::RGB_to_BGR1(colorTable + numColorsWithAlpha,
                                (const uint8_t*)palette,
                                numColors - numColorsWithAlpha);
        }
    }

    if (shouldApplyColorXformToColorTable) {
        this->applyColorXform(colorTable, colorTable, numColors);
    }

    // Pad the color table with the last color in the table (or black) in the case that
    // invalid pixel indices exceed the number of colors in the table.
    const size_t maxColors = static_cast<size_t>(1) << this->getEncodedInfo().bitsPerComponent();
    if (numColors < maxColors) {
        SkPMColor lastColor = numColors > 0 ? colorTable[numColors - 1] : SK_ColorBLACK;
        SkOpts::memset32(colorTable + numColors, lastColor, maxColors - numColors);
    }

    fColorTable.reset(new SkColorPalette(colorTable, maxColors));
    return true;
}