/*
    SPDX-FileCopyrightText: 2017 Volker Krause <vkrause@kde.org>

    SPDX-License-Identifier: MIT
*/

#include "aztecbarcode.h"
#include "bitvector_p.h"
#include "reedsolomon_p.h"
#include "prison_debug.h"

#include <QDebug>
#include <QImage>
#include <QPainter>

#include <algorithm>
#include <vector>

// see https://en.wikipedia.org/wiki/Aztec_Code for encoding tables, magic numbers, etc

using namespace Prison;

enum {
    FullMaxSize = 151,
    FullRadius = 74,
    FullGridInterval = 16,
    FullModeMessageSize = 40,
    FullLayerCount = 32,

    CompactMaxSize = 27,
    CompactRadius = 13,
    CompactModeMessageSize = 28,
    CompactLayerCount = 4
};

AztecBarcode::AztecBarcode() : AbstractBarcode(AbstractBarcode::TwoDimensions) {}
AztecBarcode::~AztecBarcode() = default;

// encoding properties depending on layer count
struct aztec_layer_property_t {
    uint8_t layer;
    uint8_t codeWordSize;
    uint16_t gf;
};

static const aztec_layer_property_t aztec_layer_properties[] = {
    { 2,   6, ReedSolomon::GF64   },
    { 8,   8, ReedSolomon::GF256  },
    { 22, 10, ReedSolomon::GF1024 },
    { 32, 12, ReedSolomon::GF4096 }
};

// amounts of bits in an Aztec code depending on layer count
static int aztecCompactDataBits(int layer)
{
    return (88 + 16 * layer) * layer;
}

static int aztecFullDataBits(int layer)
{
    return (112 + 16 * layer) * layer;
}

QImage AztecBarcode::paintImage(const QSizeF& size)
{
    Q_UNUSED(size);
    const auto inputData = aztecEncode(data().toLatin1());

    int layerCount = 0;
    int codewordCount = 0;
    int availableBits = 0;
    int stuffSize = 0; // extra bits added during bit stuffing, which might make us overun the available size
    bool compactMode = false;
    BitVector encodedData;

    do {
        layerCount = 0;
        // find the smallest layout we can put the data in, while leaving 23% for error correction
        for (auto i = 1; i <= FullLayerCount; ++i) {
            if (aztecFullDataBits(i) * 0.77 > (inputData.size() + stuffSize)) {
                layerCount = i;
                break;
            }
        }
        for (auto i = 1; i <= CompactLayerCount; ++i) {
            if (aztecCompactDataBits(i) * 0.77 > (inputData.size() + stuffSize)) {
                layerCount = i;
                compactMode = true;
                break;
            }
        }
        if (layerCount == 0) {
            qCWarning(Log) << "data too large for Aztec code" << inputData.size();
            return {};
        }

        // determine code word size
        const auto propIt = std::lower_bound(aztec_layer_properties, aztec_layer_properties + 4, layerCount, [](const aztec_layer_property_t &lhs, int rhs) {
            return lhs.layer < rhs;
        });

        // bit stuffing
        auto stuffedData = bitStuffAndPad(inputData, (*propIt).codeWordSize);
        stuffSize = stuffedData.size() - inputData.size();

        availableBits = compactMode ? aztecCompactDataBits(layerCount) : aztecFullDataBits(layerCount);
        codewordCount = stuffedData.size() / (*propIt).codeWordSize;
        const auto rsWordCount =  availableBits / (*propIt).codeWordSize - codewordCount;

        // compute error correction
        ReedSolomon rs((*propIt).gf, rsWordCount);
        const auto rsData = rs.encode(stuffedData);

        // pad with leading 0 bits to align to code word boundaries
        encodedData.reserve(availableBits);
        if (int diff = availableBits - stuffedData.size() - rsData.size()) {
            encodedData.appendMSB(0, diff);
        }
        encodedData.append(stuffedData);
        encodedData.append(rsData);

        // try again in the rare case that we overrun the available bits due to bit stuffing and padding
    } while (encodedData.size() > availableBits);

    // determine mode message
    BitVector modeMsg;
    if (compactMode) {
        modeMsg.appendMSB(layerCount - 1, 2);
        modeMsg.appendMSB(codewordCount - 1, 6);
        ReedSolomon rs(ReedSolomon::GF16, 5);
        modeMsg.append(rs.encode(modeMsg));
    } else {
        modeMsg.appendMSB(layerCount - 1, 5);
        modeMsg.appendMSB(codewordCount - 1, 11);
        ReedSolomon rs(ReedSolomon::GF16, 6);
        modeMsg.append(rs.encode(modeMsg));
    }

    // render the result
    if (compactMode) {
        QImage img(CompactMaxSize, CompactMaxSize, QImage::Format_RGB32);
        img.fill(backgroundColor());
        paintCompactGrid(&img);
        paintCompactData(&img, encodedData, layerCount);
        paintCompactModeMessage(&img, modeMsg);
        return cropAndScaleCompact(&img, layerCount);
    } else {
        QImage img(FullMaxSize, FullMaxSize, QImage::Format_RGB32);
        img.fill(backgroundColor());
        paintFullGrid(&img);
        paintFullData(&img, encodedData, layerCount);
        paintFullModeMessage(&img, modeMsg);
        return cropAndScaleFull(&img, layerCount);
    }
}

// code points and encoding modes for each of the first 127 ASCII characters, the rest is encoded in Binary mode
enum Mode {
    NoMode,
    Upper,
    Lower,
    Mixed,
    Punct,
    Digit,
    Binary,
    MODE_COUNT,
    Special
};

enum SpecialChar {
    Space,
    CarriageReturn,
    Comma,
    Dot,
    SPECIAL_CHAR_COUNT
};

struct aztec_code_t {
    uint8_t code;
    uint8_t mode;
};

static const aztec_code_t aztec_code_table[] = {
    {  0, Binary }, // 0
    {  2, Mixed },
    {  3, Mixed },
    {  4, Mixed },
    {  5, Mixed },
    {  6, Mixed },
    {  7, Mixed },
    {  8, Mixed }, // 7 BEL \a
    {  9, Mixed },
    { 10, Mixed },
    { 11, Mixed }, // 10 LF / ^J
    { 12, Mixed },
    { 13, Mixed },
    { CarriageReturn, Special }, // 13 CR / ^M - but also 1 Punct
    { 14, Binary },
    { 15, Binary },
    { 16, Binary },
    { 17, Binary },
    { 18, Binary },
    { 19, Binary },
    { 20, Binary }, // 20 ^T
    { 21, Binary },
    { 22, Binary },
    { 23, Binary },
    { 24, Binary },
    { 25, Binary },
    { 26, Binary },
    { 15, Mixed }, // 27 ^[
    { 16, Mixed },
    { 17, Mixed },
    { 18, Mixed }, // 30 ^^
    { 19, Mixed },
    { Space, Special }, // 32 SP
    {  6, Punct },
    {  7, Punct },
    {  8, Punct }, // 35 #
    {  9, Punct },
    { 10, Punct },
    { 11, Punct },
    { 12, Punct },
    { 13, Punct }, // 40 (
    { 14, Punct },
    { 15, Punct },
    { 16, Punct }, // 43 +
    { Comma, Special }, // 44 ,
    { 18, Punct }, // 45 -
    { Dot, Special }, // 46 .
    { 20, Punct }, // 47 /
    {  2, Digit }, // 48 0
    {  3, Digit },
    {  4, Digit },
    {  5, Digit },
    {  6, Digit },
    {  7, Digit },
    {  8, Digit },
    {  9, Digit },
    { 10, Digit },
    { 11, Digit }, // 57 9
    { 21, Punct }, // 58 :
    { 22, Punct }, // 59 ;
    { 23, Punct }, // 60 <
    { 24, Punct },
    { 25, Punct }, // 62 >
    { 26, Punct }, // 63 ?
    { 20, Mixed }, // 64 @
    {  2, Upper }, // 65 A
    {  3, Upper },
    {  4, Upper },
    {  5, Upper },
    {  6, Upper },
    {  7, Upper },
    {  8, Upper },
    {  9, Upper },
    { 10, Upper },
    { 11, Upper },
    { 12, Upper },
    { 13, Upper },
    { 14, Upper },
    { 15, Upper },
    { 16, Upper },
    { 17, Upper },
    { 18, Upper },
    { 19, Upper },
    { 20, Upper },
    { 21, Upper },
    { 22, Upper },
    { 23, Upper },
    { 24, Upper },
    { 25, Upper },
    { 26, Upper },
    { 27, Upper }, // 90 Z
    { 27, Punct }, // 91 [
    { 21, Mixed }, // 92 backslash
    { 28, Punct }, // 93 ]
    { 22, Mixed }, // 94 ^
    { 23, Mixed }, // 95 _
    { 24, Mixed }, // 96 `
    {  2, Lower }, // 97 a
    {  3, Lower },
    {  4, Lower },
    {  5, Lower },
    {  6, Lower },
    {  7, Lower },
    {  8, Lower },
    {  9, Lower },
    { 10, Lower },
    { 11, Lower },
    { 12, Lower },
    { 13, Lower },
    { 14, Lower },
    { 15, Lower },
    { 16, Lower },
    { 17, Lower },
    { 18, Lower },
    { 19, Lower },
    { 20, Lower },
    { 21, Lower },
    { 22, Lower },
    { 23, Lower },
    { 24, Lower },
    { 25, Lower },
    { 26, Lower },
    { 27, Lower }, // 122 z
    { 29, Punct }, // 123 {
    { 25, Mixed }, // 124 |
    { 30, Punct }, // 125 }
    { 26, Mixed }, // 126 ~
    { 27, Mixed } // 127 DEL ^?
};
Q_STATIC_ASSERT(sizeof(aztec_code_table) == 256);

static const struct {
    uint8_t c1;
    uint8_t c2;
    aztec_code_t sym;
} aztec_code_double_symbols[] = {
    { '\r', '\n', { 2, Punct } }, // CR LF
    { '.',   ' ', { 3, Punct } }, // . SP
    { ',',   ' ', { 4, Punct } }, // , SP
    { ':',   ' ', { 5, Punct } }  // : SP
};

static const int aztec_code_size[] = { 0, 5, 5, 5, 5, 4, 8 };
Q_STATIC_ASSERT(sizeof(aztec_code_size) / sizeof(int) == MODE_COUNT);

// codes for ambigious characters, ie. those that can be encoded in multiple modes
static const aztec_code_t aztec_special_chars[SPECIAL_CHAR_COUNT][MODE_COUNT - 1] = {
    /*     NoMode         Upper           Lower         Mixed          Punct          Digit   */
    { { 0, NoMode }, {  1, Upper }, {  1, Lower }, {  1, Mixed }, {  1, Upper }, {  1, Digit } }, /* SP */
    { { 0, NoMode }, {  1, Punct }, {  1, Punct }, { 14, Mixed }, {  1, Punct }, {  1, Punct } }, /* CR */
    { { 0, NoMode }, { 17, Punct }, { 17, Punct }, { 17, Punct }, { 17, Punct }, { 12, Digit } }, /* Comma */
    { { 0, NoMode }, { 19, Punct }, { 19, Punct }, { 19, Punct }, { 19, Punct }, { 13, Digit } }, /* Dot */
};

// shift code table, source mode -> target mode
// NoMode indicates shift is not available, use latch instead
static const aztec_code_t aztec_shift_codes[MODE_COUNT - 1][MODE_COUNT - 1] = {
    /*     NoMode         Upper           Lower         Mixed          Punct          Digit   */
    { { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode } },
    { { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, {  0, Punct }, { 0, NoMode } },
    { { 0, NoMode }, { 28, Upper }, { 0, NoMode }, { 0, NoMode }, {  0, Punct }, { 0, NoMode } },
    { { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, {  0, Punct }, { 0, NoMode } },
    { { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode } },
    { { 0, NoMode }, { 15, Upper }, { 0, NoMode }, { 0, NoMode }, {  0, Punct }, { 0, NoMode } }
};

// latch code table, source mode -> target mode
static const aztec_code_t aztec_latch_codes[MODE_COUNT - 1][MODE_COUNT] = {
    /*     NoMode         Upper           Lower         Mixed          Punct          Digit          Binary */
    { { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode } },
    { { 0, NoMode }, { 0, NoMode }, { 28, Lower }, { 29, Mixed }, { 29, Mixed }, { 30, Digit }, { 31, Binary } },
    { { 0, NoMode }, { 30, Digit }, { 0, NoMode }, { 29, Mixed }, { 29, Mixed }, { 30, Digit }, { 31, Binary } },
    { { 0, NoMode }, { 29, Upper }, { 28, Lower }, { 0, NoMode }, { 30, Punct }, { 28, Lower }, { 31, Binary } },
    { { 0, NoMode }, { 31, Upper }, { 31, Upper }, { 31, Upper }, { 0, NoMode }, { 31, Upper }, { 31, Upper } },
    { { 0, NoMode }, { 14, Upper }, { 14, Upper }, { 14, Upper }, { 14, Upper }, { 0, NoMode }, { 14, Upper } }
};

static Mode aztecCodeLatchTo(Mode currentMode, Mode targetMode, BitVector *v)
{
    if (currentMode == targetMode) {
        return targetMode;
    }
    const auto latchCode = aztec_latch_codes[currentMode][targetMode];
    qCDebug(Log) << "latch" << latchCode.code << aztec_code_size[currentMode];
    v->appendMSB(latchCode.code, aztec_code_size[currentMode]);
    return static_cast<Mode>(latchCode.mode);
}

static void aztecEncodeBinary(std::vector<aztec_code_t>::iterator &it, const std::vector<aztec_code_t>::iterator &end, BitVector *v)
{
    // determine length of the binary sequence
    const auto binEndIt = std::find_if(it, end, [](aztec_code_t sym) { return sym.mode != Binary; });
    const auto length = std::distance(it, binEndIt);

    // write length field
    qCDebug(Log) << "binary length" << length;
    if (length < 32) {
        v->appendMSB(length, 5);
    } else {
        v->appendMSB(0, 5);
        v->appendMSB(length - 31, 11);
    }

    // write data
    for (; it != binEndIt; ++it) {
        qCDebug(Log) << "binary data" << (*it).code;
        v->appendMSB((*it).code, 8);
    }
}

static void aztecEncodeResolveAmbigious(Mode currentMode, const std::vector<aztec_code_t>::iterator &begin, const std::vector<aztec_code_t>::iterator &end)
{
    Q_ASSERT(begin != end);
    Q_ASSERT(currentMode != (*begin).mode);
    Q_ASSERT((*begin).mode == Special);

    // forward search
    auto it = begin;
    for (; it != end && (*it).mode == Special; ++it) {
        if (aztec_special_chars[(*it).code][currentMode].mode == currentMode) {
            qCDebug(Log) << "special resolved to current mode by forward search";
            (*it).mode = aztec_special_chars[(*it).code][currentMode].mode;
            (*it).code = aztec_special_chars[(*it).code][currentMode].code;
        }
    }

    // backward search
    auto backIt = it;
    while (std::distance(begin, backIt) >= 1 && it != end) {
        --backIt;
        if ((*backIt).mode == Special && aztec_special_chars[(*backIt).code][(*it).mode].mode == (*it).mode) {
            qCDebug(Log) << "special resolved by backward search";
            (*backIt).mode = aztec_special_chars[(*backIt).code][(*it).mode].mode;
            (*backIt).code = aztec_special_chars[(*backIt).code][(*it).mode].code;
        } else {
            break;
        }
    }

    // pick one if we still have an ambigious symbol
    if ((*begin).mode != Special) {
        return;
    }
    (*begin).mode = aztec_special_chars[(*begin).code][currentMode].mode;
    (*begin).code = aztec_special_chars[(*begin).code][currentMode].code;
    it = begin + 1;
    if (it != end && (*it).mode == Special) {
        aztecEncodeResolveAmbigious(static_cast<Mode>((*begin).mode), it, end);
    }
}

static Mode aztecNextMode(Mode currentMode, const std::vector<aztec_code_t>::iterator &nextSym, const std::vector<aztec_code_t>::iterator &end, bool &tryShift)
{
    Q_ASSERT(currentMode != (*nextSym).mode);
    Q_ASSERT(nextSym != end);
    Q_ASSERT((*nextSym).mode != Special);
    auto it = nextSym;
    ++it;
    if (it != end && (*it).mode == Special) {
        aztecEncodeResolveAmbigious(static_cast<Mode>((*nextSym).mode), it, end);
    }

    if ((it == end || (*it).mode == currentMode) && std::distance(nextSym, it) == 1) {
        tryShift = true;
    }

    qCDebug(Log) << currentMode << (*nextSym).mode << tryShift << std::distance(nextSym, it);
    return static_cast<Mode>((*nextSym).mode);
}

BitVector AztecBarcode::aztecEncode(const QByteArray &data) const
{
    // phase one: translate single and double chars to code points
    std::vector<aztec_code_t> codes;
    codes.reserve(data.size());
    for (int i = 0; i < data.size(); ++i) {
        const uint8_t c1 = data.at(i);
        // double char codes
        if (i < data.size() - 1) {
            const uint8_t c2 = data.at(i + 1);
            bool found = false;
            for (const auto &dblCode : aztec_code_double_symbols) {
                if (dblCode.c1 != c1 || dblCode.c2 != c2) {
                    continue;
                }
                codes.push_back(dblCode.sym);
                ++i;
                found = true;
            }
            if (found) {
                continue;
            }
        }

        // > 127 binary-only range
        if (c1 > 127) {
            codes.push_back({c1, Binary});
        // encodable single ASCII character
        } else {
            codes.push_back(aztec_code_table[c1]);
        }
    }

    // phase two: insert shift and latch codes, translate to bit stream
    Mode currentMode = Upper;
    BitVector result;
    for (auto it = codes.begin(); it != codes.end();) {
        if ((*it).mode == Binary) {
            auto newMode = aztecCodeLatchTo(currentMode, Binary, &result);
            while (newMode != Binary) {
                currentMode = newMode;
                newMode = aztecCodeLatchTo(currentMode, Binary, &result);
            }
            aztecEncodeBinary(it, codes.end(), &result);
            continue;
        }
        // resolve special codes
        if ((*it).mode == Special) {
            aztecEncodeResolveAmbigious(currentMode, it, codes.end());
        }

        // deal with mode changes
        Mode nextMode = currentMode;
        if ((*it).mode != currentMode) {
            bool tryShift = false;
            const auto newMode = aztecNextMode(currentMode, it, codes.end(), tryShift);

            // shift to new mode if desired and possible
            if (tryShift && aztec_shift_codes[currentMode][newMode].mode != NoMode) {
                qCDebug(Log) << "shift" << aztec_shift_codes[currentMode][newMode].code << aztec_code_size[currentMode];
                result.appendMSB(aztec_shift_codes[currentMode][newMode].code, aztec_code_size[currentMode]);
                currentMode = newMode;
            }

            // latch to new mode
            while (currentMode != newMode && newMode != NoMode && currentMode != NoMode) {
                currentMode = aztecCodeLatchTo(currentMode, newMode, &result);
                nextMode = currentMode;
            }
        }

        qCDebug(Log) << (*it).code << aztec_code_size[currentMode];
        result.appendMSB((*it).code, aztec_code_size[currentMode]);
        ++it;

        currentMode = nextMode;
    }

    return result;
}

BitVector AztecBarcode::bitStuffAndPad(const BitVector& input, int codeWordSize) const
{
    BitVector res;
    res.reserve(input.size());

    // bit stuff codewords with leading codeWordSize 0/1 bits
    int i = 0;
    while(i < input.size() - (codeWordSize - 1)) {
        int v = input.valueAtMSB(i, codeWordSize - 1);
        res.appendMSB(v, codeWordSize - 1);
        i += codeWordSize - 1;
        if (v == 0) {
            res.appendBit(true);
        } else if (v == (1 << (codeWordSize - 1)) - 1) {
            res.appendBit(false);
        } else {
            res.appendBit(input.at(i++));
        }
    }
    while (i < input.size()) {
        res.appendBit(input.at(i++));
    }

    // check if we are code word aligned already
    const auto trailingBits = res.size() % codeWordSize;
    if (!trailingBits) { // nothing to pad
        return res;
    }

    // pad with ones to nearest code word boundary
    // last bit has to be zero if we'd otherwise would have all ones though
    bool allOnes = true;
    for (int i = res.size() - trailingBits; i < res.size(); ++i) {
        allOnes &= res.at(i);
    }
    while (res.size() % codeWordSize) {
        if ((res.size() % codeWordSize) == (codeWordSize - 1)) {
            res.appendBit(allOnes ? false : true);
        } else {
            res.appendBit(true);
        }
    }

    return res;
}

void AztecBarcode::paintFullGrid(QImage *img) const
{
    QPainter p(img);
    p.translate(img->width() / 2, img->height() / 2);

    // alignment grids
    QPen pen(foregroundColor());
    pen.setDashPattern({1, 1});
    p.setPen(pen);
    for (int i = 0; i < img->width() / 2; i += FullGridInterval) {
        p.drawLine(-i, -FullRadius, -i, FullRadius);
        p.drawLine( i, -FullRadius,  i, FullRadius);
        p.drawLine(-FullRadius, -i, FullRadius, -i);
        p.drawLine(-FullRadius,  i, FullRadius,  i);
    }

    // bullseye background
    p.setBrush(backgroundColor());
    p.setPen(Qt::NoPen);
    p.drawRect(-7, -7, 14, 14);

    // bullseye
    p.setBrush(Qt::NoBrush);
    p.setPen(foregroundColor());
    p.drawPoint(0, 0);
    p.drawRect(-2, -2, 4, 4);
    p.drawRect(-4, -4, 8, 8);
    p.drawRect(-6, -6, 12, 12);

    // bullseye orientation marker
    p.drawRect(-7, -7, 1, 1);
    p.drawRect(7, -7, 0, 1);
    p.drawPoint(7, 6);
}

static const int aztecFullLayerOffset[] = {
//   0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26 27 28 29 30 31
    66, 64, 62, 60, 57, 55, 53, 51, 49, 47, 45, 42, 40, 38, 36, 34, 32, 30, 28, 25, 23, 21, 19, 17, 15, 13, 10, 8, 6, 4, 2, 0
};

void AztecBarcode::paintFullData(QImage* img, const BitVector &data, int layerCount) const
{
    QPainter p(img);
    p.setPen(foregroundColor());

    auto it = data.begin();
    for (int layer = layerCount - 1; layer >= 0; --layer) {
        const auto x1 = aztecFullLayerOffset[layer];
        const auto y1 = x1;
        const auto gridInMiddle = (x1 - FullRadius) % FullGridInterval == 0;
        const auto x2 = gridInMiddle ? x1 + 2 : x1 + 1;
        const auto segmentLength = FullMaxSize - 2 * y1 - 2 - (gridInMiddle ? 1 : 0);

        for (int rotation = 0; rotation < 4; ++rotation) {
            p.resetTransform();
            p.translate(img->width() / 2, img->height() / 2);
            p.rotate(-90 * rotation);
            p.translate(-img->width() / 2, -img->height() / 2);

            for (int i = 0; it != data.end(); ++i, ++it) {
                const auto x = (i % 2 == 0) ? x1 : x2;
                auto y = i / 2 + y1;
                if (((y - FullRadius - 1) % FullGridInterval) == 0) { // skip grid lines
                    ++y;
                    i += 2;
                }
                if (y >= y1 + segmentLength) {
                    break;
                }
                if (*it) {
                    p.drawPoint(x, y);
                }
            }
        }
    }
}

void AztecBarcode::paintFullModeMessage(QImage *img, const BitVector &modeData) const
{
    Q_ASSERT(modeData.size() == FullModeMessageSize);

    QPainter p(img);
    p.setPen(foregroundColor());

    auto it = modeData.begin();
    for (int rotation = 0; rotation < 4; ++rotation) {
        p.resetTransform();
        p.translate(img->width() / 2, img->height() / 2);
        p.rotate(90 * rotation);

        for (int i = -5; i <= 5; ++i) {
            if (i == 0) { // skip grid line
                continue;
            }
            if (*it) {
                p.drawPoint(i, -7);
            }
            ++it;
        }
    }
}

QImage AztecBarcode::cropAndScaleFull(QImage *img, int layerCount)
{
    const auto offset = aztecFullLayerOffset[layerCount - 1];
    const auto minSize = FullMaxSize - 2 * offset;

    QImage out(minSize, minSize, img->format());
    QPainter p(&out);
    p.setRenderHint(QPainter::SmoothPixmapTransform, false);
    const auto srcRect = img->rect().adjusted(offset, offset, -offset, -offset);
    p.drawImage(out.rect(), *img, srcRect);
    return out;
}

void AztecBarcode::paintCompactGrid(QImage *img) const
{
    QPainter p(img);
    p.translate(img->width() / 2, img->height() / 2);

    // bullseye
    p.setPen(foregroundColor());
    p.drawPoint(0, 0);
    p.drawRect(-2, -2, 4, 4);
    p.drawRect(-4, -4, 8, 8);

    // bullseye orientation marker
    p.drawRect(-5, -5, 1, 1);
    p.drawRect(5, -5, 0, 1);
    p.drawPoint(5, 4);
}

static const int aztecCompactLayerOffset[] = {
    6, 4, 2, 0
};

void AztecBarcode::paintCompactData(QImage *img, const BitVector &data, int layerCount) const
{
    QPainter p(img);
    p.setPen(foregroundColor());

    auto it = data.begin();
    for (int layer = layerCount - 1; layer >= 0; --layer) {
        const auto x1 = aztecCompactLayerOffset[layer];
        const auto y1 = x1;
        const auto x2 = x1 + 1;
        const auto segmentLength = CompactMaxSize - 2 * y1 - 2;

        for (int rotation = 0; rotation < 4; ++rotation) {
            p.resetTransform();
            p.translate(img->width() / 2, img->height() / 2);
            p.rotate(-90 * rotation);
            p.translate(-img->width() / 2, -img->height() / 2);

            for (int i = 0; it != data.end(); ++i, ++it) {
                const auto x = (i % 2 == 0) ? x1 : x2;
                auto y = i / 2 + y1;
                if (y >= y1 + segmentLength) {
                    break;
                }
                if (*it) {
                    p.drawPoint(x, y);
                }
            }
        }
    }
}

void AztecBarcode::paintCompactModeMessage(QImage *img, const BitVector &modeData) const
{
    Q_ASSERT(modeData.size() == CompactModeMessageSize);

    QPainter p(img);
    p.setPen(foregroundColor());

    auto it = modeData.begin();
    for (int rotation = 0; rotation < 4; ++rotation) {
        p.resetTransform();
        p.translate(img->width() / 2, img->height() / 2);
        p.rotate(90 * rotation);

        for (int i = -3; i <= 3; ++i) {
            if (*it) {
                p.drawPoint(i, -5);
            }
            ++it;
        }
    }
}

QImage AztecBarcode::cropAndScaleCompact(QImage *img, int layerCount)
{
    const auto offset = aztecCompactLayerOffset[layerCount - 1];
    const auto minSize = CompactMaxSize - 2 * offset;

    QImage out(minSize, minSize, img->format());
    QPainter p(&out);
    p.setRenderHint(QPainter::SmoothPixmapTransform, false);
    const auto srcRect = img->rect().adjusted(offset, offset, -offset, -offset);
    p.drawImage(out.rect(), *img, srcRect);
    return out;
}
