/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <vcl/filter/PngImageWriter.hxx>
#include <png.h>
#include <vcl/BitmapWriteAccess.hxx>
#include <vcl/bitmap.hxx>
#include <vcl/BitmapTools.hxx>
#include <sal/log.hxx>
#include <rtl/crc.h>

namespace
{
void combineScanlineChannels(BitmapScopedReadAccess& pAccess, Scanline pScanline,
                             std::vector<std::remove_pointer_t<Scanline>>& pResult,
                             sal_uInt32 nBitmapWidth, int nChannels)
{
    for (sal_uInt32 i = 0; i < nBitmapWidth; ++i)
    {
        BitmapColor aCol = pAccess->GetPixelFromData(pScanline, i);
        pResult[i * nChannels] = aCol.GetRed(); // R
        pResult[i * nChannels + 1] = aCol.GetGreen(); // G
        pResult[i * nChannels + 2] = aCol.GetBlue(); // B
        if (nChannels == 4)
            pResult[i * 4 + 3] = aCol.GetAlpha(); // A
    }
}
}

namespace vcl
{
static void lclWriteStream(png_structp pPng, png_bytep pData, png_size_t pDataSize)
{
    png_voidp pIO = png_get_io_ptr(pPng);

    if (pIO == nullptr)
        return;

    SvStream* pStream = static_cast<SvStream*>(pIO);

    sal_Size nBytesWritten = pStream->WriteBytes(pData, pDataSize);

    if (nBytesWritten != pDataSize)
        png_error(pPng, "Write Error");
}

static void writeFctlChunk(std::vector<uint8_t>& aFctlChunk, sal_uInt32 nSequenceNumber, Size aSize,
                           Point aOffset, sal_uInt16 nDelayNum, sal_uInt16 nDelayDen,
                           Disposal nDisposeOp, Blend nBlendOp)
{
    if (aFctlChunk.size() != 26)
        aFctlChunk.resize(26);

    sal_uInt32 nWidth = aSize.Width();
    sal_uInt32 nHeight = aSize.Height();
    sal_uInt32 nXOffset = aOffset.X();
    sal_uInt32 nYOffset = aOffset.Y();

    // Writing each byte separately instead of using memcpy here for clarity
    // about PNG chunks using big endian

    // Write sequence number
    aFctlChunk[0] = (nSequenceNumber >> 24) & 0xFF;
    aFctlChunk[1] = (nSequenceNumber >> 16) & 0xFF;
    aFctlChunk[2] = (nSequenceNumber >> 8) & 0xFF;
    aFctlChunk[3] = nSequenceNumber & 0xFF;

    // Write width
    aFctlChunk[4] = (nWidth >> 24) & 0xFF;
    aFctlChunk[5] = (nWidth >> 16) & 0xFF;
    aFctlChunk[6] = (nWidth >> 8) & 0xFF;
    aFctlChunk[7] = nWidth & 0xFF;

    // Write height
    aFctlChunk[8] = (nHeight >> 24) & 0xFF;
    aFctlChunk[9] = (nHeight >> 16) & 0xFF;
    aFctlChunk[10] = (nHeight >> 8) & 0xFF;
    aFctlChunk[11] = nHeight & 0xFF;

    // Write x offset
    aFctlChunk[12] = (nXOffset >> 24) & 0xFF;
    aFctlChunk[13] = (nXOffset >> 16) & 0xFF;
    aFctlChunk[14] = (nXOffset >> 8) & 0xFF;
    aFctlChunk[15] = nXOffset & 0xFF;

    // Write y offset
    aFctlChunk[16] = (nYOffset >> 24) & 0xFF;
    aFctlChunk[17] = (nYOffset >> 16) & 0xFF;
    aFctlChunk[18] = (nYOffset >> 8) & 0xFF;
    aFctlChunk[19] = nYOffset & 0xFF;

    // Write delay numerator
    aFctlChunk[20] = (nDelayNum >> 8) & 0xFF;
    aFctlChunk[21] = nDelayNum & 0xFF;

    // Write delay denominator
    aFctlChunk[22] = (nDelayDen >> 8) & 0xFF;
    aFctlChunk[23] = nDelayDen & 0xFF;

    // Write disposal method
    aFctlChunk[24] = static_cast<uint8_t>(nDisposeOp);

    // Write blend operation
    aFctlChunk[25] = static_cast<uint8_t>(nBlendOp);
}

static bool pngWrite(SvStream& rStream, const Graphic& rGraphic, int nCompressionLevel,
                     bool bInterlaced, bool bTranslucent,
                     const std::vector<PngChunk>& aAdditionalChunks)
{
    if (rGraphic.IsNone())
        return false;

    sal_uInt32 nSequenceNumber = 0;
    const bool bIsApng = rGraphic.IsAnimated();
    Animation aAnimation = bIsApng ? rGraphic.GetAnimation() : Animation();

    png_structp pPng = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);

    if (!pPng)
        return false;

    png_infop pInfo = png_create_info_struct(pPng);
    if (!pInfo)
    {
        png_destroy_write_struct(&pPng, nullptr);
        return false;
    }

    basegfx::B2DSize const aPPM = rGraphic.GetPPM();
    png_set_pHYs(pPng, pInfo, std::round(aPPM.getWidth()), std::round(aPPM.getHeight()),
                 PNG_RESOLUTION_METER);

    Bitmap aBitmap(rGraphic.GetBitmap());

    if (!bTranslucent && aBitmap.HasAlpha())
    {
        // strip out the alpha layer
        aBitmap = aBitmap.CreateColorBitmap();
    }

    BitmapScopedReadAccess pAccess(aBitmap);

    if (setjmp(png_jmpbuf(pPng)))
    {
        pAccess.reset();
        png_destroy_read_struct(&pPng, &pInfo, nullptr);
        return false;
    }

    // Set our custom stream writer
    png_set_write_fn(pPng, &rStream, lclWriteStream, nullptr);

    {
        bool bCombineChannels = false;
        Size aSize = aBitmap.GetSizePixel();

        int bitDepth = -1;
        int colorType = -1;

        /* PNG_COLOR_TYPE_GRAY (1, 2, 4, 8, 16)
           PNG_COLOR_TYPE_GRAY_ALPHA (8, 16)
           PNG_COLOR_TYPE_PALETTE (bit depths 1, 2, 4, 8)
           PNG_COLOR_TYPE_RGB (bit_depths 8, 16)
           PNG_COLOR_TYPE_RGB_ALPHA (bit_depths 8, 16)
           PNG_COLOR_MASK_PALETTE
           PNG_COLOR_MASK_COLOR
           PNG_COLOR_MASK_ALPHA
        */
        auto eScanlineFormat = pAccess->GetScanlineFormat();
        switch (eScanlineFormat)
        {
            case ScanlineFormat::N8BitPal:
            {
                assert(!aBitmap.HasAlpha());
                assert(pAccess->HasPalette());
                // Calling aBitmap.HasGreyPalette8Bit() hits an assert when
                // using Skia in a debug build so query the palette through
                // the bitmap read access object.
                if (!pAccess->GetPalette().IsGreyPalette8Bit())
                    colorType = PNG_COLOR_TYPE_PALETTE;
                else
                    colorType = PNG_COLOR_TYPE_GRAY;
                bitDepth = 8;
                break;
            }
            case ScanlineFormat::N24BitTcBgr:
            {
                assert(!aBitmap.HasAlpha());
                colorType = PNG_COLOR_TYPE_RGB;
                bitDepth = 8;
                png_set_bgr(pPng);
                break;
            }
            case ScanlineFormat::N24BitTcRgb:
            {
                assert(!aBitmap.HasAlpha());
                colorType = PNG_COLOR_TYPE_RGB;
                bitDepth = 8;
                break;
            }
            case ScanlineFormat::N32BitTcBgra:
            {
                assert(aBitmap.HasAlpha());
                colorType = PNG_COLOR_TYPE_RGB_ALPHA;
                bitDepth = 8;
                bCombineChannels = true;
                break;
            }
            case ScanlineFormat::N32BitTcBgrx:
            {
                assert(!aBitmap.HasAlpha());
                colorType = PNG_COLOR_TYPE_RGB;
                bitDepth = 8;
                bCombineChannels = true;
                break;
            }
            case ScanlineFormat::N32BitTcRgba:
            {
                assert(aBitmap.HasAlpha());
                colorType = PNG_COLOR_TYPE_RGB_ALPHA;
                bitDepth = 8;
                bCombineChannels = true;
                break;
            }
            case ScanlineFormat::N32BitTcRgbx:
            {
                assert(!aBitmap.HasAlpha());
                colorType = PNG_COLOR_TYPE_RGB;
                bitDepth = 8;
                bCombineChannels = true;
                break;
            }
            default:
            {
                assert(false);
                return false;
            }
        }

        if (aBitmap.GetPrefMapMode().GetMapUnit() == MapUnit::Map100thMM)
        {
            Size aPrefSize(aBitmap.GetPrefSize());
            if (aPrefSize.Width() && aPrefSize.Height())
            {
                sal_uInt32 nPrefSizeX = o3tl::convert(aSize.Width(), 100000, aPrefSize.Width());
                sal_uInt32 nPrefSizeY = o3tl::convert(aSize.Height(), 100000, aPrefSize.Height());
                png_set_pHYs(pPng, pInfo, nPrefSizeX, nPrefSizeY, 1);
            }
        }

        png_set_compression_level(pPng, nCompressionLevel);

        int interlaceType = bInterlaced ? PNG_INTERLACE_ADAM7 : PNG_INTERLACE_NONE;
        int compressionType = PNG_COMPRESSION_TYPE_DEFAULT;
        int filterMethod = PNG_FILTER_TYPE_DEFAULT;

        // Convert BitmapPalette to png_color*
        if (colorType == PNG_COLOR_TYPE_PALETTE)
        {
            // Reserve enough space for 3 channels for each palette entry
            auto aBitmapPalette = pAccess->GetPalette();
            auto nEntryCount = aBitmapPalette.GetEntryCount();
            std::unique_ptr<png_color[]> aPngPaletteArray(new png_color[nEntryCount * 3]);
            for (sal_uInt16 i = 0; i < nEntryCount; i++)
            {
                aPngPaletteArray[i].red = aBitmapPalette[i].GetRed();
                aPngPaletteArray[i].green = aBitmapPalette[i].GetGreen();
                aPngPaletteArray[i].blue = aBitmapPalette[i].GetBlue();
            }
            // Palette is copied over so it can be safely discarded
            png_set_PLTE(pPng, pInfo, aPngPaletteArray.get(), nEntryCount);
        }

        png_set_IHDR(pPng, pInfo, aSize.Width(), aSize.Height(), bitDepth, colorType, interlaceType,
                     compressionType, filterMethod);

        png_write_info(pPng, pInfo);

        if (bIsApng)
        {
            // Write acTL chunk
            sal_uInt32 nNumFrames = aAnimation.Count();
            sal_uInt32 nNumPlays = aAnimation.GetLoopCount();

            std::vector<uint8_t> aActlChunk;
            aActlChunk.resize(8);

            // Write number of frames
            aActlChunk[0] = (nNumFrames >> 24) & 0xFF;
            aActlChunk[1] = (nNumFrames >> 16) & 0xFF;
            aActlChunk[2] = (nNumFrames >> 8) & 0xFF;
            aActlChunk[3] = nNumFrames & 0xFF;

            // Write number of plays
            aActlChunk[4] = (nNumPlays >> 24) & 0xFF;
            aActlChunk[5] = (nNumPlays >> 16) & 0xFF;
            aActlChunk[6] = (nNumPlays >> 8) & 0xFF;
            aActlChunk[7] = nNumPlays & 0xFF;

            png_write_chunk(pPng, reinterpret_cast<png_const_bytep>("acTL"),
                            reinterpret_cast<png_const_bytep>(aActlChunk.data()),
                            aActlChunk.size());

            // Write first frame fcTL chunk which is corresponding to the IDAT chunk
            std::vector<uint8_t> aFctlChunk;
            const AnimationFrame& rFirstFrame = *aAnimation.GetAnimationFrames()[0];
            writeFctlChunk(aFctlChunk, nSequenceNumber++, rFirstFrame.maSizePixel,
                           rFirstFrame.maPositionPixel, rFirstFrame.mnWait, 100,
                           rFirstFrame.meDisposal, rFirstFrame.meBlend);

            png_write_chunk(pPng, reinterpret_cast<png_const_bytep>("fcTL"),
                            reinterpret_cast<png_const_bytep>(aFctlChunk.data()),
                            aFctlChunk.size());
        }

        int nNumberOfPasses = 1;

        Scanline pSourcePointer;

        tools::Long nHeight = pAccess->Height();

        auto nChannels = png_get_channels(pPng, pInfo);
        for (int nPass = 0; nPass < nNumberOfPasses; nPass++)
        {
            for (tools::Long y = 0; y < nHeight; y++)
            {
                pSourcePointer = pAccess->GetScanline(y);
                Scanline pFinalPointer = pSourcePointer;
                std::vector<std::remove_pointer_t<Scanline>> aCombinedChannels;
                if (bCombineChannels)
                {
                    auto nBitmapWidth = pAccess->Width();
                    // Allocate enough size to fit all channels
                    aCombinedChannels.resize(nBitmapWidth * nChannels);
                    // Combine color and alpha channels
                    combineScanlineChannels(pAccess, pSourcePointer, aCombinedChannels,
                                            nBitmapWidth, nChannels);
                    pFinalPointer = aCombinedChannels.data();
                }
                png_write_row(pPng, pFinalPointer);
            }
        }
    }

    if (bIsApng)
    {
        // Already wrote first frame as an IDAT chunk
        // Need to write the rest of the frames as fcTL & fdAT chunks
        const auto& rFrames = aAnimation.GetAnimationFrames();

        for (uint32_t i = 0; i < rFrames.size() - 1; i++)
        {
            const AnimationFrame& rCurrentFrame = *rFrames[1 + i];
            SvMemoryStream aStream;

            if (!pngWrite(aStream, rCurrentFrame.maBitmap, nCompressionLevel, bInterlaced,
                          bTranslucent, {}))
                return false;

            std::vector<uint8_t> aFdatChunk;

            aStream.SetEndian(SvStreamEndian::BIG);

            aStream.Seek(STREAM_SEEK_TO_BEGIN);
            aStream.Seek(8); // Skip PNG signature

            while (aStream.good())
            {
                sal_uInt32 nChunkSize;
                char sChunkName[4] = { 0 };
                aStream.ReadUInt32(nChunkSize);
                aStream.ReadBytes(sChunkName, 4);

                if (std::string(sChunkName, 4) == "IDAT")
                {
                    // 4 extra bytes for the sequence number
                    aFdatChunk.resize(nChunkSize + 4);
                    aStream.ReadBytes(aFdatChunk.data() + 4, nChunkSize);
                    break;
                }
                else
                {
                    aStream.SeekRel(nChunkSize + 4);
                }
            }

            std::vector<uint8_t> aFctlChunk;
            writeFctlChunk(aFctlChunk, nSequenceNumber++, rCurrentFrame.maSizePixel,
                           rCurrentFrame.maPositionPixel, rCurrentFrame.mnWait, 100,
                           rCurrentFrame.meDisposal, rCurrentFrame.meBlend);

            // Write sequence number
            aFdatChunk[0] = nSequenceNumber >> 24;
            aFdatChunk[1] = nSequenceNumber >> 16;
            aFdatChunk[2] = nSequenceNumber >> 8;
            aFdatChunk[3] = nSequenceNumber;
            nSequenceNumber++;

            png_write_chunk(pPng, reinterpret_cast<png_const_bytep>("fcTL"),
                            reinterpret_cast<png_const_bytep>(aFctlChunk.data()),
                            aFctlChunk.size());
            png_write_chunk(pPng, reinterpret_cast<png_const_bytep>("fdAT"),
                            reinterpret_cast<png_const_bytep>(aFdatChunk.data()),
                            aFdatChunk.size());
        }
    }

    if (!aAdditionalChunks.empty())
    {
        for (const auto& aChunk : aAdditionalChunks)
        {
            png_write_chunk(pPng, aChunk.name.data(), aChunk.data.data(), aChunk.size);
        }
    }

    png_write_end(pPng, pInfo);

    png_destroy_write_struct(&pPng, &pInfo);

    return true;
}

void PngImageWriter::setParameters(css::uno::Sequence<css::beans::PropertyValue> const& rParameters)
{
    for (auto const& rValue : rParameters)
    {
        if (rValue.Name == "Compression")
            rValue.Value >>= mnCompressionLevel;
        else if (rValue.Name == "Interlaced")
            rValue.Value >>= mbInterlaced;
        else if (rValue.Name == "Translucent")
        {
            tools::Long nTmp = 0;
            rValue.Value >>= nTmp;
            if (!nTmp)
                mbTranslucent = false;
        }
        else if (rValue.Name == "AdditionalChunks")
        {
            css::uno::Sequence<css::beans::PropertyValue> aAdditionalChunkSequence;
            if (rValue.Value >>= aAdditionalChunkSequence)
            {
                for (const auto& rAdditionalChunk : aAdditionalChunkSequence)
                {
                    if (rAdditionalChunk.Name.getLength() == 4)
                    {
                        vcl::PngChunk aChunk;
                        for (sal_Int32 k = 0; k < 4; k++)
                        {
                            aChunk.name[k] = static_cast<sal_uInt8>(rAdditionalChunk.Name[k]);
                        }
                        aChunk.name[4] = '\0';

                        css::uno::Sequence<sal_Int8> aByteSeq;
                        if (rAdditionalChunk.Value >>= aByteSeq)
                        {
                            sal_uInt32 nChunkSize = aByteSeq.getLength();
                            aChunk.size = nChunkSize;
                            if (nChunkSize)
                            {
                                const sal_Int8* pSource = aByteSeq.getConstArray();
                                aChunk.data = std::vector<sal_uInt8>(pSource, pSource + nChunkSize);
                                maAdditionalChunks.push_back(std::move(aChunk));
                            }
                        }
                    }
                }
            }
        }
    }
}

PngImageWriter::PngImageWriter(SvStream& rStream)
    : mrStream(rStream)
    , mnCompressionLevel(6)
    , mbInterlaced(false)
    , mbTranslucent(true)
{
}

bool PngImageWriter::write(const Graphic& rGraphic)
{
    return pngWrite(mrStream, rGraphic, mnCompressionLevel, mbInterlaced, mbTranslucent,
                    maAdditionalChunks);
}

} // namespace vcl

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
