#include "core/compressors/Stc4.h"

#include <cstring>

#include "core/compressors/Stc0.h"

namespace bmp2tile
{

namespace
{

constexpr int MaxGroupRepetitions = 31;
constexpr uint8_t GroupMatchesMask = 0x20;
constexpr uint8_t InvGroupMatchesMask = 0x30;

}  // namespace

std::vector<uint8_t> CompressStc4(const std::vector<uint8_t>& input)
{
    std::vector<uint8_t> output;
    output.reserve(input.size() / 4 * 5 + 8);  // same worst case as STC0, plus a little slack for run bytes

    uint8_t buffer[4] = {0, 0, 0, 0};  // the most recently *emitted* group's raw bytes; unread while pos==0
    int groupRepetitions = 0;

    for (size_t pos = 0; pos + 4 <= input.size(); pos += 4)
    {
        const uint8_t* group = &input[pos];
        const bool isFirstGroup = (pos == 0);  // matches the reference's (tilenum+i)>0 check

        if (!isFirstGroup)
        {
            if (std::memcmp(buffer, group, 4) == 0)
            {
                ++groupRepetitions;
                if (groupRepetitions == MaxGroupRepetitions)
                {
                    output.push_back(static_cast<uint8_t>(groupRepetitions));
                    groupRepetitions = 0;
                }
                continue;  // this group is fully accounted for by the run-length counter
            }
            else if (groupRepetitions > 0)
            {
                output.push_back(static_cast<uint8_t>(groupRepetitions));
                groupRepetitions = 0;
                // no continue -- this (non-matching) group still needs its own encoding below
            }
        }

        uint8_t values[4];
        int valuesCount = 0;
        const uint8_t mask = EncodeStc0Group(group, values, valuesCount);

        // A "group match" is only worth it if it takes fewer bytes than the
        // plain STC0 encoding above would (valuesCount raw bytes) -- and
        // only possible to compare against `buffer` from the second group
        // onward.
        if (!isFirstGroup && valuesCount >= 2)
        {
            int groupUnmatches = 0;
            uint8_t groupUnmatchesMask = 0;
            for (int j = 0; j < 4; ++j)
            {
                if (buffer[j] != group[j])
                {
                    ++groupUnmatches;
                    groupUnmatchesMask = static_cast<uint8_t>(groupUnmatchesMask | (1 << (3 - j)));
                }
            }

            int invGroupUnmatches = 0;
            uint8_t invGroupUnmatchesMask = 0;
            for (int j = 0; j < 4; ++j)
            {
                if (buffer[j] != static_cast<uint8_t>(~group[j]))
                {
                    ++invGroupUnmatches;
                    invGroupUnmatchesMask = static_cast<uint8_t>(invGroupUnmatchesMask | (1 << (3 - j)));
                }
            }

            // Only use the inverted match if it's strictly better than the
            // regular (non-inverted) one.
            if (invGroupUnmatches < groupUnmatches)
            {
                if (invGroupUnmatches < valuesCount)
                {
                    output.push_back(static_cast<uint8_t>(InvGroupMatchesMask | invGroupUnmatchesMask));
                    for (int j = 0; j < 4; ++j)
                    {
                        if (buffer[j] != static_cast<uint8_t>(~group[j]))
                        {
                            output.push_back(group[j]);
                        }
                    }
                    std::memcpy(buffer, group, 4);
                    continue;
                }
            }
            else
            {
                if (groupUnmatches < valuesCount)
                {
                    output.push_back(static_cast<uint8_t>(GroupMatchesMask | groupUnmatchesMask));
                    for (int j = 0; j < 4; ++j)
                    {
                        if (buffer[j] != group[j])
                        {
                            output.push_back(group[j]);
                        }
                    }
                    std::memcpy(buffer, group, 4);
                    continue;
                }
            }
        }

        // Neither a repeat nor a worthwhile group match -- fall back to
        // plain STC0 encoding of this group.
        std::memcpy(buffer, group, 4);
        output.push_back(mask);
        for (int k = 0; k < valuesCount; ++k)
        {
            output.push_back(values[k]);
        }
    }

    if (groupRepetitions > 0)
    {
        output.push_back(static_cast<uint8_t>(groupRepetitions));
    }
    output.push_back(0x00);  // end-of-data marker

    return output;
}

}  // namespace bmp2tile
