//=============================================================================
//
// Adventure Game Studio (AGS)
//
// Copyright (C) 1999-2011 Chris Jones and 2011-2026 various contributors
// The full list of copyright holders can be found in the Copyright.txt
// file, which is part of this source code distribution.
//
// The AGS source code is provided under the Artistic License 2.0.
// A copy of this license can be found in the file License.txt and at
// https://opensource.org/license/artistic-2-0/
//
//=============================================================================
#include "ac/spritefile.h"
#include <algorithm>
#include <array>
#include <time.h>
#include "core/assetmanager.h"
#include "gfx/bitmap.h"
#include "util/compress.h"
#include "util/file.h"
#include "util/memory_compat.h"
#include "util/memorystream.h"

namespace AGS
{
namespace Common
{

const char *spriteFileSig = " Sprite File ";
const char *spindexid = "SPRINDEX";

// TODO: should not be part of SpriteFile, but rather some asset management class?
const String SpriteFile::DefaultSpriteFileName = "acsprset.spr";
const String SpriteFile::DefaultSpriteIndexName = "sprindex.dat";


// Image buffer pointer, a helper struct that eases switching
// between intermediate buffers when loading, saving or converting an image.
template <typename T> struct ImBufferPtrT
{
    T        Buf = nullptr;
    size_t   Size = 0;
    int      BPP = 1; // byte per pixel

    ImBufferPtrT() = default;
    ImBufferPtrT(T buf, size_t sz, int bpp) : Buf(buf), Size(sz), BPP(bpp) {}
};
typedef ImBufferPtrT<uint8_t*> ImBufferPtr;
typedef ImBufferPtrT<const uint8_t*> ImBufferCPtr;


// Finds the given color's index in the palette, or returns SIZE_MAX if such color is not there
static size_t lookup_palette(uint32_t col, uint32_t palette[256], uint32_t ncols)
{
    for (size_t i = 0; i < ncols; ++i)
        if (palette[i] == col) return i;
    return SIZE_MAX;
}

// Converts a 16/32-bit image into the indexed 8-bit pixel data with palette;
// NOTE: the palette will contain colors in the same format as the source image.
// only succeeds if the total number of colors used in the image is < 257.
static bool CreateIndexedBitmap(const Bitmap *image, std::vector<uint8_t> &dst_data,
    uint32_t palette[256], uint32_t &pal_count)
{
    const int src_bpp = image->GetBPP();
    if (src_bpp < 2) { assert(0); return false; }
    const size_t src_size = image->GetWidth() * image->GetHeight() * image->GetBPP();
    const size_t dst_size = image->GetWidth() * image->GetHeight();
    dst_data.resize(dst_size);
    const uint8_t *src = image->GetData(), *src_end = src + src_size;
    uint8_t *dst = &dst_data[0], *dst_end = dst + dst_size;
    pal_count = 0;

    for (; src < src_end && dst < dst_end; src += src_bpp)
    {
        uint32_t col;
        size_t pal_n;
        switch (src_bpp)
        {
        case 2:
            col = *((const uint16_t*)src);
            pal_n = lookup_palette(col, palette, pal_count);
            break;
        case 4:
            col = *((const uint32_t*)src);
            pal_n = lookup_palette(col, palette, pal_count);
            break;
        default: assert(0); return false;
        }
        
        if (pal_n == SIZE_MAX)
        {
            if (pal_count == 256) return false;
            pal_n = pal_count;
            palette[pal_count++] = col;
        }
        *(dst++) = (uint8_t)pal_n;
    }
    return true;
}

// Unpacks an indexed image's pixel data into the 16/32-bit image;
// NOTE: the palette is expected to contain colors in the same format as the destination.
static void UnpackIndexedBitmap(Bitmap *image, const uint8_t *data, size_t data_size,
                                const std::array<uint32_t, 256> &palette, uint32_t pal_count)
{
    assert(pal_count > 0 && pal_count <= 256);
    if (pal_count == 0 || pal_count > 256) return; // meaningless

    const uint8_t bpp = static_cast<uint8_t>(image->GetBPP());
    const size_t dst_size = image->GetWidth() * image->GetHeight() * image->GetBPP();
    uint8_t *dst = image->GetDataForWriting();
    uint8_t const *dst_end = dst + dst_size;

    switch (bpp)
    {
        case 2:
            for (size_t p = 0; (p < data_size) && (dst < dst_end); ++p, dst += bpp) {
                uint8_t index = data[p];
                assert(index < pal_count);
                uint32_t color = palette[index];
                *((uint16_t *) dst) = color;
            }
        break;
        case 4:
            for (size_t p = 0; (p < data_size) && (dst < dst_end); ++p, dst += bpp) {
                uint8_t index = data[p];
                assert(index < pal_count);
                uint32_t color = palette[index];
                *((uint32_t*)dst) = color;
            }
        break;
        default: assert(0); return;
    }
}


static inline SpriteFormat PaletteFormatForBPP(int bpp)
{
    switch (bpp)
    {
    case 1: return kSprFmt_PaletteRgb888;
    case 2: return kSprFmt_PaletteRgb565;
    case 4: return kSprFmt_PaletteArgb8888;
    default: return kSprFmt_Undefined;
    }
}

static inline uint8_t GetPaletteBPP(SpriteFormat fmt)
{
    switch (fmt)
    {
    case kSprFmt_PaletteRgb888: return 3;
    case kSprFmt_PaletteArgb8888: return 4;
    case kSprFmt_PaletteRgb565: return 2;
    default: return 0; // means no palette
    }
}


SpriteFile::SpriteFile()
{
    _curPos = -2;
}

HError SpriteFile::OpenFile(std::unique_ptr<Stream> &&sprite_file,
    std::unique_ptr<Stream> &&index_file, std::vector<Size> &metrics)
{
    Close();

    assert(sprite_file);
    if (!sprite_file)
        return new Error("Invalid spritefile stream.");

    _stream = std::move(sprite_file);

    soff_t spr_initial_offs = _stream->GetPosition();
    _version = (SpriteFileVersion)_stream->ReadInt16();
    // read the "Sprite File" signature
    char buff[20];
    _stream->ReadArray(&buff[0], 13, 1);

    if (_version < kSprfVersion_Uncompressed || _version > kSprfVersion_Current)
    {
        _stream.reset();
        return new Error(String::FromFormat("Unsupported spriteset format (requested %d, supported %d - %d).", _version,
            kSprfVersion_Uncompressed, kSprfVersion_Current));
    }

    // unknown version
    buff[13] = 0;
    if (strcmp(buff, spriteFileSig))
    {
        _stream.reset();
        return new Error("Uknown spriteset format.");
    }

    int spriteFileID = 0;
    _storeFlags = 0;
    if (_version < kSprfVersion_Compressed)
    {
        _compress = kSprCompress_None;
        // skip the palette
        _stream->Seek(256 * 3); // sizeof(RGB) * 256
    }
    else if (_version == kSprfVersion_Compressed)
    {
        _compress = kSprCompress_RLE;
    }
    else if (_version >= kSprfVersion_Last32bit)
    {
        _compress = (SpriteCompression)_stream->ReadInt8();
        spriteFileID = _stream->ReadInt32();
    }

    sprkey_t topmost;
    if (_version < kSprfVersion_HighSpriteLimit)
        topmost = (uint16_t)_stream->ReadInt16();
    else
        topmost = _stream->ReadInt32();
    if (_version < kSprfVersion_Uncompressed)
        topmost = 200;

    _spriteData.resize(topmost + 1);
    metrics.resize(topmost + 1);

    // Version 12+: read global store flags
    if (_version >= kSprfVersion_StorageFormats)
    {
        _storeFlags = _stream->ReadInt8();
        _stream->ReadInt8(); // reserved
        _stream->ReadInt8();
        _stream->ReadInt8();
    }

    // if there is a sprite index file, use it
    if (LoadSpriteIndexFile(std::move(index_file), spriteFileID,
        spr_initial_offs, topmost, metrics))
    {
        // Succeeded
        return HError::None();
    }

    // Failed, index file is invalid; index sprites manually
    return RebuildSpriteIndex(_stream.get(), topmost, metrics);
}

void SpriteFile::Close()
{
    _stream.reset();
    _spriteData.clear();
    _version = kSprfVersion_Undefined;
    _storeFlags = 0;
    _compress = kSprCompress_None;
    _curPos = -2;
}

int SpriteFile::GetStoreFlags() const
{
    return _storeFlags;
}

SpriteCompression SpriteFile::GetSpriteCompression() const
{
    return _compress;
}

sprkey_t SpriteFile::GetTopmostSprite() const
{
    return (sprkey_t)_spriteData.size() - 1;
}

bool SpriteFile::LoadSpriteIndexFile(std::unique_ptr<Stream> &&fidx,
    int expectedFileID, soff_t spr_initial_offs, sprkey_t topmost, std::vector<Size> &metrics)
{
    if (!fidx)
    {
        return false;
    }

    char buffer[9];
    // check "SPRINDEX" id
    fidx->ReadArray(&buffer[0], strlen(spindexid), 1);
    buffer[8] = 0;
    if (strcmp(buffer, spindexid))
    {
        return false;
    }
    // check version
    SpriteIndexFileVersion vers = (SpriteIndexFileVersion)fidx->ReadInt32();
    if (vers < kSpridxfVersion_Initial || vers > kSpridxfVersion_Current)
    {
        return false;
    }
    if (vers >= kSpridxfVersion_Last32bit)
    {
        if (fidx->ReadInt32() != expectedFileID)
        {
            return false;
        }
    }

    sprkey_t topmost_index = fidx->ReadInt32();
    // end index+1 should be the same as num sprites
    if (fidx->ReadInt32() != topmost_index + 1)
    {
        return false;
    }

    if (topmost_index != topmost)
    {
        return false;
    }

    sprkey_t numsprits = topmost_index + 1;
    std::vector<int16_t> rspritewidths; rspritewidths.resize(numsprits);
    std::vector<int16_t> rspriteheights; rspriteheights.resize(numsprits);
    std::vector<soff_t>  spriteoffs; spriteoffs.resize(numsprits);

    fidx->ReadArrayOfInt16(&rspritewidths[0], numsprits);
    fidx->ReadArrayOfInt16(&rspriteheights[0], numsprits);
    if (vers <= kSpridxfVersion_Last32bit)
    {
        for (sprkey_t i = 0; i < numsprits; ++i)
            spriteoffs[i] = fidx->ReadInt32();
    }
    else // large file support
    {
        fidx->ReadArrayOfInt64(&spriteoffs[0], numsprits);
    }

    for (sprkey_t i = 0; i <= topmost_index; ++i)
    {
        if (spriteoffs[i] != 0)
        {
            _spriteData[i].Offset = spriteoffs[i] + spr_initial_offs;
            metrics[i].Width = rspritewidths[i];
            metrics[i].Height = rspriteheights[i];
        }
    }
    return true;
}

static inline void ReadSprHeader(SpriteDatHeader &hdr, Stream *in,
    const SpriteFileVersion ver, SpriteCompression gl_compress)
{
    int bpp = in->ReadInt8();
    SpriteFormat sformat = (SpriteFormat)in->ReadInt8();
    // note we MUST read first 2 * int8 before skipping rest
    if (bpp == 0) { hdr = SpriteDatHeader(); return; } // empty slot
    int pal_count = 0;
    SpriteCompression compress = gl_compress;
    if (ver >= kSprfVersion_StorageFormats)
    {
        pal_count = (uint8_t)in->ReadInt8() + 1; // saved as (count - 1)
        compress = (SpriteCompression)in->ReadInt8();
    }
    int w = in->ReadInt16();
    int h = in->ReadInt16();
    hdr = SpriteDatHeader(bpp, sformat, pal_count, compress, w, h);
}

HError SpriteFile::RebuildSpriteIndex(Stream *in, sprkey_t topmost, std::vector<Size> &metrics)
{
    topmost = std::min(topmost, (sprkey_t)_spriteData.size() - 1);
    for (sprkey_t i = 0; !in->EOS() && (i <= topmost); ++i)
    {
        _spriteData[i].Offset = in->GetPosition();
        SpriteDatHeader hdr;
        ReadSprHeader(hdr, _stream.get(), _version, _compress);
        if (hdr.BPP == 0) continue; // empty slot, this is normal
        int pal_bpp = GetPaletteBPP(hdr.SFormat);
        if (pal_bpp > 0) in->Seek(hdr.PalCount * pal_bpp); // skip palette
        size_t data_sz =
            ((_version >= kSprfVersion_StorageFormats) || _compress != kSprCompress_None) ?
            (uint32_t)in->ReadInt32() : hdr.Width * hdr.Height * hdr.BPP;
        in->Seek(data_sz); // skip image data
        metrics[i].Width = hdr.Width;
        metrics[i].Height = hdr.Height;
    }
    return HError::None();
}

HError SpriteFile::LoadSprite(sprkey_t index, Common::Bitmap *&sprite)
{
    sprite = nullptr;
    if (index < 0 || (size_t)index >= _spriteData.size())
        return new Error(String::FromFormat("LoadSprite: slot index %d out of bounds (%d - %d).",
            index, 0, _spriteData.size() - 1));

    if (_spriteData[index].Offset == 0)
        return HError::None(); // sprite is not in file

    SeekToSprite(index);
    _curPos = -2; // mark undefined pos

    SpriteDatHeader hdr;
    ReadSprHeader(hdr, _stream.get(), _version, _compress);
    if (hdr.BPP == 0) return HError::None(); // empty slot, this is normal
    int bpp = hdr.BPP, w = hdr.Width, h = hdr.Height;
    std::unique_ptr<Bitmap> image(BitmapHelper::CreateBitmap(w, h, bpp * 8));
    if (image == nullptr)
    {
        return new Error(String::FromFormat("LoadSprite: failed to allocate bitmap %d (%dx%d%d).",
            index, w, h, bpp * 8));
    }
    ImBufferPtr im_data(image->GetDataForWriting(), w * h * bpp, bpp);
    // (Optional) Handle storage options, reverse
    std::vector<uint8_t> indexed_buf;
    std::array<uint32_t, 256> palette {};
    uint32_t pal_bpp = GetPaletteBPP(hdr.SFormat);
    if (pal_bpp > 0)
    { // read palette if format assumes one
        switch (pal_bpp)
        {
        case 2: for (uint32_t i = 0; i < hdr.PalCount; ++i) { palette[i] = _stream->ReadInt16(); }
            break;
        case 4: for (uint32_t i = 0; i < hdr.PalCount; ++i) { palette[i] = _stream->ReadInt32(); }
            break;
        default: assert(0); break;
        }
        indexed_buf.resize(w * h);
        im_data = ImBufferPtr(&indexed_buf[0], indexed_buf.size(), 1);
    }
    // (Optional) Decompress the image data into the temp buffer
    size_t in_data_size =
        ((_version >= kSprfVersion_StorageFormats) || _compress != kSprCompress_None) ?
        (uint32_t)_stream->ReadInt32() : (w * h * bpp);
    if (hdr.Compress != kSprCompress_None)
    {
        // TODO: rewrite this to only make a choice once the SpriteFile is initialized
        // and use either function ptr or a decompressing stream class object
        if (in_data_size == 0)
        {
            return new Error(String::FromFormat("LoadSprite: bad compressed data for sprite %d.", index));
        }
        bool result;
        switch (hdr.Compress)
        {
        case kSprCompress_RLE: result = rle_decompress(im_data.Buf, im_data.Size, im_data.BPP, _stream.get());
            break;
        case kSprCompress_LZW: result = lzw_decompress(im_data.Buf, im_data.Size, im_data.BPP, _stream.get(), in_data_size);
            break;
        case kSprCompress_Deflate: result = inflate_decompress(im_data.Buf, im_data.Size, im_data.BPP, _stream.get(), in_data_size);
            break;
        default: assert(!"Unsupported compression type!"); result = false; break;
        }
        // TODO: test that not more than data_size was read!
        if (!result)
        {
            return new Error(String::FromFormat("LoadSprite: failed to decompress pixel array for sprite %d.", index));
        }
    }
    // Otherwise (no compression) read directly
    else
    {
        switch (im_data.BPP)
        {
        case 1: _stream->Read(im_data.Buf, im_data.Size);
            break;
        case 2: _stream->ReadArrayOfInt16(
                reinterpret_cast<int16_t*>(im_data.Buf), im_data.Size / sizeof(int16_t));
            break;
        case 4: _stream->ReadArrayOfInt32(
                reinterpret_cast<int32_t*>(im_data.Buf), im_data.Size / sizeof(int32_t));
            break;
        default: assert(0); break;
        }
    }
    // Finally revert storage options
    if (pal_bpp > 0)
    {
        UnpackIndexedBitmap(image.get(), im_data.Buf, im_data.Size, palette, hdr.PalCount);
    }

    sprite = image.release(); // FIXME: pass unique_ptr in this function
    _curPos = index + 1; // mark correct pos
    return HError::None();
}

HError SpriteFile::LoadRawData(sprkey_t index, SpriteDatHeader &hdr, std::vector<uint8_t> &data)
{
    hdr = SpriteDatHeader();
    data.resize(0);
    if (index < 0 || (size_t)index >= _spriteData.size())
        return new Error(String::FromFormat("LoadSprite: slot index %d out of bounds (%d - %d).",
            index, 0, _spriteData.size() - 1));

    if (_spriteData[index].Offset == 0)
        return HError::None(); // sprite is not in file

    SeekToSprite(index);
    _curPos = -2; // mark undefined pos

    ReadSprHeader(hdr, _stream.get(), _version, _compress);
    if (hdr.BPP == 0) return HError::None(); // empty slot, this is normal
    size_t data_size = 0;
    soff_t data_pos = _stream->GetPosition();
    // Optional palette
    size_t pal_size = hdr.PalCount * GetPaletteBPP(hdr.SFormat);
    data_size += pal_size;
    _stream->Seek(pal_size);
    // Pixel data
    if ((_version >= kSprfVersion_StorageFormats) || _compress != kSprCompress_None)
        data_size += (uint32_t)_stream->ReadInt32() + sizeof(uint32_t);
    else
        data_size += hdr.Width * hdr.Height * hdr.BPP;
    // Seek back and read all at once
    data.resize(data_size);
    _stream->Seek(data_pos, kSeekBegin);
    _stream->Read(&data[0], data_size);

    _curPos = index + 1; // mark correct pos
    return HError::None();
}

void SpriteFile::SeekToSprite(sprkey_t index)
{
    // If we didn't just load the previous sprite, seek to it
    if (index != _curPos)
    {
        _stream->Seek(_spriteData[index].Offset, kSeekBegin);
        _curPos = index;
    }
}


// Finds the topmost occupied slot index
static sprkey_t FindTopmostSprite(const std::vector<std::pair<bool, Bitmap*>> &sprites)
{
    sprkey_t topmost = -1;
    for (sprkey_t i = 0; i < static_cast<sprkey_t>(sprites.size()); ++i)
        if (sprites[i].first)
            topmost = i;
    return topmost;
}

int SaveSpriteFile(const String &save_to_file,
    const std::vector<std::pair<bool, Bitmap*>> &sprites,
    SpriteFile *read_from_file,
    int store_flags, SpriteCompression compress, SpriteFileIndex &index)
{
    std::unique_ptr<Stream> output(File::CreateFile(save_to_file));
    if (output == nullptr)
        return -1;

    sprkey_t lastslot = FindTopmostSprite(sprites);
    SpriteFileWriter writer(std::move(output));
    writer.Begin(store_flags, compress, lastslot);

    std::unique_ptr<Bitmap> temp_bmp; // for disposing temp sprites
    std::vector<uint8_t> membuf; // for loading raw sprite data

    const bool diff_compress =
        read_from_file &&
        (read_from_file->GetSpriteCompression() != compress ||
        read_from_file->GetStoreFlags() != store_flags);

    for (sprkey_t i = 0; i <= lastslot; ++i)
    {
        if (!sprites[i].first)
        { // empty slot
            writer.WriteEmptySlot();
            continue;
        }

        Bitmap *image = sprites[i].second;
        // if compression setting is different, load the sprite into memory
        // (otherwise we will be able to simply copy bytes from one file to another
        if ((image == nullptr) && diff_compress)
        {
            read_from_file->LoadSprite(i, image);
            temp_bmp.reset(image);
        }

        // if managed to load an image - save it according the new compression settings
        if (image != nullptr)
        {
            writer.WriteBitmap(image);
            continue;
        }
        else if (diff_compress)
        {
            // sprite doesn't exist
            writer.WriteEmptySlot();
            continue;
        }

        // Not in memory - and same compression option;
        // Directly copy the sprite bytes from the input file to the output
        SpriteDatHeader hdr;
        read_from_file->LoadRawData(i, hdr, membuf);
        if (hdr.BPP == 0)
        { // empty slot
            writer.WriteEmptySlot();
            continue;
        }
        writer.WriteRawData(hdr, &membuf[0], membuf.size());
    }
    writer.Finalize();

    index = writer.GetIndex();
    return 0;
}

int SaveSpriteIndex(const String &filename, const SpriteFileIndex &index)
{
    // write the sprite index file
    auto out = File::CreateFile(filename);
    if (!out)
        return -1;
    // write "SPRINDEX" id
    out->WriteArray(spindexid, strlen(spindexid), 1);
    // write version
    out->WriteInt32(kSpridxfVersion_Current);
    out->WriteInt32(index.SpriteFileIDCheck);
    // write last sprite number and num sprites, to verify that
    // it matches the spr file
    out->WriteInt32(index.GetLastSlot());
    out->WriteInt32(index.GetCount());
    if (index.GetCount() > 0)
    {
        out->WriteArrayOfInt16(&index.Widths[0], index.Widths.size());
        out->WriteArrayOfInt16(&index.Heights[0], index.Heights.size());
        out->WriteArrayOfInt64(&index.Offsets[0], index.Offsets.size());
    }
    return 0;
}


void SpriteFileWriter::Begin(int store_flags, SpriteCompression compress, sprkey_t last_slot)
{
    if (!_out) return;
    _index.SpriteFileIDCheck = (int)time(nullptr);
    _storeFlags = store_flags;
    _compress = compress;

    // sprite file version
    _out->WriteInt16(kSprfVersion_Current);
    _out->WriteArray(spriteFileSig, strlen(spriteFileSig), 1);
    _out->WriteInt8(_compress);
    _out->WriteInt32(_index.SpriteFileIDCheck);

    // Remember and write provided "last slot" index,
    // but if it's not set (< 0) then we will have to return back later
    // and write correct one; this is done in Finalize().
    _lastSlotPos = _out->GetPosition();
    _out->WriteInt32(last_slot);

    _out->WriteInt8(_storeFlags);
    _out->WriteInt8(0); // reserved
    _out->WriteInt8(0);
    _out->WriteInt8(0);

    if (last_slot >= 0)
    { // allocate buffers to store the indexing info
        sprkey_t numsprits = last_slot + 1;
        _index.Offsets.reserve(numsprits);
        _index.Widths.reserve(numsprits);
        _index.Heights.reserve(numsprits);
    }
}

void SpriteFileWriter::WriteBitmap(const Bitmap *image)
{
    if (!_out) return;
    int bpp = image->GetBPP();
    int w = image->GetWidth();
    int h = image->GetHeight();
    ImBufferCPtr im_data(image->GetData(), w * h * bpp, bpp);

    // (Optional) Handle storage options
    std::vector<uint8_t> indexed_buf;
    uint32_t palette[256];
    uint32_t pal_count = 0;
    SpriteFormat sformat = kSprFmt_Undefined;
    if ((_storeFlags & kSprStore_OptimizeForSize) != 0 && (image->GetBPP() > 1))
    { // Try to store this sprite as an indexed bitmap
        uint32_t gen_pal_count;
        if (CreateIndexedBitmap(image, indexed_buf, palette, gen_pal_count) && gen_pal_count > 0)
        { // Test the resulting size, and switch if the paletted image is less
            if (im_data.Size > (indexed_buf.size() + gen_pal_count * image->GetBPP()))
            {
                im_data = ImBufferCPtr(&indexed_buf[0], indexed_buf.size(), 1);
                sformat = PaletteFormatForBPP(image->GetBPP());
                pal_count = gen_pal_count;
            }
        }
    }
    // (Optional) Compress the image data into the temp buffer
    SpriteCompression compress = kSprCompress_None;
    if (_compress != kSprCompress_None)
    {
        // TODO: rewrite this to only make a choice once the SpriteFile is initialized
        // and use either function ptr or a decompressing stream class object
        compress = _compress;
        Stream mems(std::make_unique<VectorStream>(_membuf, kStream_Write));
        bool result;
        switch (compress)
        {
        case kSprCompress_RLE: result = rle_compress(im_data.Buf, im_data.Size, im_data.BPP, &mems);
            break;
        case kSprCompress_LZW: result = lzw_compress(im_data.Buf, im_data.Size, im_data.BPP, &mems);
            break;
        case kSprCompress_Deflate: result = deflate_compress(im_data.Buf, im_data.Size, im_data.BPP, &mems);
            break;
        default: assert(!"Unsupported compression type!"); result = false; break;
        }
        // mark to write as a plain byte array
        im_data = result ? ImBufferCPtr(&_membuf[0], _membuf.size(), 1) : ImBufferCPtr();
    }

    // Write the final data
    SpriteDatHeader hdr(bpp, sformat, pal_count, compress, w, h);
    WriteSpriteData(hdr, im_data.Buf, im_data.Size, im_data.BPP, palette);
    _membuf.clear();
}

static inline void WriteSprHeader(const SpriteDatHeader &hdr, Stream *out)
{
    out->WriteInt8(hdr.BPP);
    out->WriteInt8(hdr.SFormat);
    out->WriteInt8(hdr.PalCount > 0 ? (uint8_t)(hdr.PalCount - 1) : 0);
    out->WriteInt8(hdr.Compress);
    out->WriteInt16(hdr.Width);
    out->WriteInt16(hdr.Height);
}

void SpriteFileWriter::WriteSpriteData(const SpriteDatHeader &hdr,
    const uint8_t *im_data, size_t im_data_sz, int im_bpp,
    const uint32_t palette[256])
{
    // Add index entry and write resulting data to the stream
    soff_t sproff = _out->GetPosition();
    _index.Offsets.push_back(sproff);
    _index.Widths.push_back(hdr.Width);
    _index.Heights.push_back(hdr.Height);
    WriteSprHeader(hdr, _out.get());
    // write palette, if available
    int pal_bpp = GetPaletteBPP(hdr.SFormat);
    if (pal_bpp > 0)
    {
        assert(hdr.PalCount > 0);
        switch (pal_bpp)
        {
        case 2: for (uint32_t i = 0; i < hdr.PalCount; ++i) { _out->WriteInt16(palette[i]); }
            break;
        case 4: for (uint32_t i = 0; i < hdr.PalCount; ++i) { _out->WriteInt32(palette[i]); }
            break;
        }
    }
    // write the image pixel data
    _out->WriteInt32(im_data_sz);
    switch (im_bpp)
    {
    case 1: _out->Write(im_data, im_data_sz);
        break;
    case 2: _out->WriteArrayOfInt16(reinterpret_cast<const int16_t*>(im_data),
            im_data_sz / sizeof(int16_t));
        break;
    case 4: _out->WriteArrayOfInt32(reinterpret_cast<const int32_t*>(im_data),
            im_data_sz / sizeof(int32_t));
        break;
    default: assert(0); break;
    }
}

void SpriteFileWriter::WriteEmptySlot()
{
    if (!_out) return;
    soff_t sproff = _out->GetPosition();
    _out->WriteInt16(0); // write invalid color depth to mark empty slot
    _index.Offsets.push_back(sproff);
    _index.Widths.push_back(0);
    _index.Heights.push_back(0);
}

void SpriteFileWriter::WriteRawData(const SpriteDatHeader &hdr, const uint8_t *data, size_t data_sz)
{
    if (!_out) return;
    soff_t sproff = _out->GetPosition();
    _index.Offsets.push_back(sproff);
    _index.Widths.push_back(hdr.Width);
    _index.Heights.push_back(hdr.Height);
    WriteSprHeader(hdr, _out.get());
    _out->Write(data, data_sz);
}

void SpriteFileWriter::Finalize()
{
    if (!_out || _lastSlotPos < 0) return;
    _out->Seek(_lastSlotPos, kSeekBegin);
    _out->WriteInt32(_index.GetLastSlot());
    _out.reset();
}

} // namespace Common
} // namespace AGS
