#include "core/compressors/PsGaiden.h"

#include <array>
#include <map>
#include <stdexcept>

namespace bmp2tile
{

namespace
{

// Ported from Utils::deinterleave in bmp2tilecompressors/compressors/
// utils.cpp, specialised to a fixed 32-byte tile with interleaving=4:
// turns row-major "4 bitplane bytes per row x 8 rows" (PackTilePlanar's
// output) into 4 contiguous 8-byte bitplanes, e.g. bytes
// P0P1P2P3 P0P1P2P3 ... (8 groups) become P0P0P0P0P0P0P0P0 P1P1... .
std::array<uint8_t, 32> Deinterleave4(const std::array<uint8_t, 32>& buffer)
{
    std::array<uint8_t, 32> result{};
    constexpr size_t interleaving = 4;
    const size_t bitplaneSize = buffer.size() / interleaving;
    for (size_t src = 0; src < buffer.size(); ++src)
    {
        const size_t dest = src / interleaving + (src % interleaving) * bitplaneSize;
        result[dest] = buffer[src];
    }
    return result;
}

// Ported from findMostCommonValue. Ties are broken by lowest byte value,
// matching the original's std::map<uint8_t,int> iteration order (only
// updates on strictly-greater count).
void FindMostCommonValue(const std::array<uint8_t, 32>& data, size_t offset, uint8_t& value, int& count)
{
    std::map<uint8_t, int> counts;
    for (int i = 0; i < 8; ++i)
    {
        counts[data[offset + static_cast<size_t>(i)]]++;
    }
    count = 0;
    for (const auto& pair : counts)
    {
        if (pair.second > count)
        {
            value = pair.first;
            count = pair.second;
        }
    }
}

// Ported from countMatches.
int CountMatches(const std::array<uint8_t, 32>& data, size_t meOffset, size_t otherOffset, bool invert)
{
    int count = 0;
    const uint8_t mask = invert ? 0xFF : 0x00;
    for (int i = 0; i < 8; ++i)
    {
        if (data[meOffset + static_cast<size_t>(i)] == (data[otherOffset + static_cast<size_t>(i)] ^ mask))
        {
            ++count;
        }
    }
    return count;
}

// Ported from compressTile. `source` is one tile, already deinterleaved
// (4 contiguous 8-byte bitplanes). Appends this tile's compressed bytes
// to `destination`.
//
// Per-bitplane method byte (2 bits each, packed MSB-first across the 4
// bitplanes into one leading byte per tile):
//   %00 = all-zero bitplane, no data follows
//   %01 = all-0xFF bitplane, no data follows
//   %10 = "compressed": a sub-method byte (whole-bitplane duplicate/
//         inverse-duplicate of an earlier bitplane in this tile, or a
//         common-byte-plus-bitmask-plus-exceptions encoding) follows
//   %11 = raw: the bitplane's 8 bytes follow uncompressed
void CompressTile(const std::array<uint8_t, 32>& source, std::vector<uint8_t>& destination)
{
    uint8_t bitplaneMethods = 0;
    std::vector<uint8_t> tileData;

    for (int bitplaneIndex = 0; bitplaneIndex < 4; ++bitplaneIndex)
    {
        const size_t bitplaneOffset = static_cast<size_t>(bitplaneIndex) * 8;

        uint8_t mostCommonByte = 0;
        int mostCommonByteCount = 0;
        FindMostCommonValue(source, bitplaneOffset, mostCommonByte, mostCommonByteCount);

        int otherBitplaneMatchIndex = 0;
        int otherBitplaneMatchCount = 0;
        bool otherBitplaneMatchInverse = false;
        for (int otherBitplaneIndex = 0; otherBitplaneIndex < bitplaneIndex; ++otherBitplaneIndex)
        {
            const size_t otherOffset = static_cast<size_t>(otherBitplaneIndex) * 8;

            int count = CountMatches(source, bitplaneOffset, otherOffset, false);
            if (count > otherBitplaneMatchCount)
            {
                otherBitplaneMatchIndex = otherBitplaneIndex;
                otherBitplaneMatchCount = count;
                otherBitplaneMatchInverse = false;
            }
            count = CountMatches(source, bitplaneOffset, otherOffset, true);
            if (count > otherBitplaneMatchCount)
            {
                otherBitplaneMatchIndex = otherBitplaneIndex;
                otherBitplaneMatchCount = count;
                otherBitplaneMatchInverse = true;
            }
        }

        bitplaneMethods = static_cast<uint8_t>(bitplaneMethods << 2);

        if (mostCommonByteCount == 8 && mostCommonByte == 0x00)
        {
            bitplaneMethods = static_cast<uint8_t>(bitplaneMethods | 0x00);  // all 0 = %00, no data
        }
        else if (mostCommonByteCount == 8 && mostCommonByte == 0xFF)
        {
            bitplaneMethods = static_cast<uint8_t>(bitplaneMethods | 0x01);  // all ff = %01, no data
        }
        else if (mostCommonByteCount <= 2 && otherBitplaneMatchCount <= 2)
        {
            bitplaneMethods = static_cast<uint8_t>(bitplaneMethods | 0x03);  // raw = %11
            tileData.insert(tileData.end(), source.begin() + static_cast<long>(bitplaneOffset),
                             source.begin() + static_cast<long>(bitplaneOffset) + 8);
        }
        else
        {
            bitplaneMethods = static_cast<uint8_t>(bitplaneMethods | 0x02);  // compressed = %10

            if (otherBitplaneMatchCount == 8)
            {
                // %000f00nn = whole bitplane duplicate (f = inverted, nn = source bitplane)
                const uint8_t tag = otherBitplaneMatchInverse ? 0x10 : 0x00;
                tileData.push_back(static_cast<uint8_t>(tag | otherBitplaneMatchIndex));
            }
            else if (otherBitplaneMatchCount > mostCommonByteCount)
            {
                // %001000nn / %010000nn = copy (optionally inverted) bytes from another bitplane
                const uint8_t tag = otherBitplaneMatchInverse ? 0x40 : 0x20;
                tileData.push_back(static_cast<uint8_t>(tag | otherBitplaneMatchIndex));

                const uint8_t mask = otherBitplaneMatchInverse ? 0xFF : 0x00;
                const size_t otherOffset = static_cast<size_t>(otherBitplaneMatchIndex) * 8;

                uint8_t bitmask = 0;
                for (int i = 0; i < 8; ++i)
                {
                    bitmask = static_cast<uint8_t>(bitmask << 1);
                    if (source[bitplaneOffset + static_cast<size_t>(i)] == (source[otherOffset + static_cast<size_t>(i)] ^ mask))
                    {
                        bitmask |= 1;
                    }
                }
                tileData.push_back(bitmask);

                for (int i = 0; i < 8; ++i)
                {
                    if (source[bitplaneOffset + static_cast<size_t>(i)] != (source[otherOffset + static_cast<size_t>(i)] ^ mask))
                    {
                        tileData.push_back(source[bitplaneOffset + static_cast<size_t>(i)]);
                    }
                }
            }
            else
            {
                // common byte + bitmask + exceptions
                uint8_t bitmask = 0;
                for (int i = 0; i < 8; ++i)
                {
                    bitmask = static_cast<uint8_t>(bitmask << 1);
                    if (source[bitplaneOffset + static_cast<size_t>(i)] == mostCommonByte)
                    {
                        bitmask |= 1;
                    }
                }
                tileData.push_back(bitmask);
                tileData.push_back(mostCommonByte);

                for (int i = 0; i < 8; ++i)
                {
                    if (source[bitplaneOffset + static_cast<size_t>(i)] != mostCommonByte)
                    {
                        tileData.push_back(source[bitplaneOffset + static_cast<size_t>(i)]);
                    }
                }
            }
        }
    }

    destination.push_back(bitplaneMethods);
    destination.insert(destination.end(), tileData.begin(), tileData.end());
}

}  // namespace

std::vector<uint8_t> CompressPsGaidenTiles(const std::vector<Tile>& tiles)
{
    if (tiles.size() > 0xFFFF)
    {
        throw std::runtime_error("PSGaiden can't compress more than 65535 tiles.");
    }

    std::vector<uint8_t> destination;
    destination.push_back(static_cast<uint8_t>(tiles.size() & 0xFF));
    destination.push_back(static_cast<uint8_t>((tiles.size() >> 8) & 0xFF));

    for (const Tile& tile : tiles)
    {
        const std::array<uint8_t, 32> interleaved = PackTilePlanar(tile);
        const std::array<uint8_t, 32> deinterleaved = Deinterleave4(interleaved);
        CompressTile(deinterleaved, destination);
    }

    return destination;
}

}  // namespace bmp2tile
