/*
    kimgio module for SGI images
    SPDX-FileCopyrightText: 2004 Melchior FRANZ <mfranz@kde.org>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

/* this code supports:
 * reading:
 *     everything, except images with 1 dimension or images with
 *     mapmode != NORMAL (e.g. dithered); Images with 16 bit
 *     precision or more than 4 layers are stripped down.
 * writing:
 *     Run Length Encoded (RLE) or Verbatim (uncompressed)
 *     (whichever is smaller)
 *
 * Please report if you come across rgb/rgba/sgi/bw files that aren't
 * recognized. Also report applications that can't deal with images
 * saved by this filter.
 */

#include "rgb_p.h"
#include "scanlineconverter_p.h"
#include "util_p.h"

#include <cstring>

#include <QColorSpace>
#include <QImage>
#include <QList>
#include <QLoggingCategory>
#include <QMap>

#ifdef QT_DEBUG
Q_LOGGING_CATEGORY(LOG_RGBPLUGIN, "kf.imageformats.plugins.rgb", QtDebugMsg)
#else
Q_LOGGING_CATEGORY(LOG_RGBPLUGIN, "kf.imageformats.plugins.rgb", QtWarningMsg)
#endif

class RLEData : public QList<uchar>
{
public:
    RLEData()
    {
    }
    RLEData(const uchar *d, uint l, uint o)
        : _offset(o)
    {
        for (uint i = 0; i < l; i++) {
            append(d[i]);
        }
    }
    bool operator<(const RLEData &) const;
    void write(QDataStream &s);
    uint offset() const
    {
        return _offset;
    }

private:
    uint _offset;
};

class RLEMap : public QMap<RLEData, uint>
{
public:
    RLEMap()
        : _counter(0)
        , _offset(0)
    {
    }
    uint insert(const uchar *d, uint l);
    QList<const RLEData *> vector();
    void setBaseOffset(uint o)
    {
        _offset = o;
    }

private:
    uint _counter;
    uint _offset;
};

class SGIImagePrivate
{
public:
    SGIImagePrivate();
    ~SGIImagePrivate();

    bool readImage(QImage &);
    bool writeImage(const QImage &);

    bool isValid() const;
    bool isSupported() const;

    bool peekHeader(QIODevice *device);

    QSize size() const;
    QImage::Format format() const;

    void setDevice(QIODevice *device);

private:
    enum {
        NORMAL,
        DITHERED,
        SCREEN,
        COLORMAP,
    }; // colormap
    QIODevice *_dev;
    QDataStream _stream;

    quint16 _magic = 0;
    quint8 _rle = 0;
    quint8 _bpc = 0;
    quint16 _dim = 0;
    quint16 _xsize = 0;
    quint16 _ysize = 0;
    quint16 _zsize = 0;
    quint32 _pixmin = 0;
    quint32 _pixmax = 0;
    char _imagename[80];
    quint32 _colormap = 0;
    quint8 _unused[404];
    quint32 _unused32 = 0;

    quint32 *_starttab;
    quint32 *_lengthtab;
    QByteArray _data;
    QByteArray::Iterator _pos;
    RLEMap _rlemap;
    QList<const RLEData *> _rlevector;
    uint _numrows;

    bool readData(QImage &);
    bool getRow(uchar *dest);
    bool readHeader();

    static bool readHeader(QDataStream &ds, SGIImagePrivate *sgi);

    bool writeHeader();
    bool writeRle();
    bool writeVerbatim(const QImage &, const QImage::Format&, const QColorSpace&);
    bool scanData(const QImage &, const QImage::Format&, const QColorSpace&);
    uint compact(uchar *, uchar *);
    uchar intensity(uchar);
};

SGIImagePrivate::SGIImagePrivate()
    : _dev(nullptr)
    , _starttab(nullptr)
    , _lengthtab(nullptr)
{
    std::memset(_imagename, 0, sizeof(_imagename));
    std::memset(_unused, 0, sizeof(_unused));
}

SGIImagePrivate::~SGIImagePrivate()
{
    delete[] _starttab;
    delete[] _lengthtab;
}

///////////////////////////////////////////////////////////////////////////////

void SGIImagePrivate::setDevice(QIODevice *device)
{
    _dev = device;
    _stream.setDevice(_dev);
}

bool SGIImagePrivate::getRow(uchar *dest)
{
    int n;
    int i;
    if (!_rle) {
        for (i = 0; i < _xsize; i++) {
            if (_pos >= _data.end()) {
                return false;
            }
            dest[i] = uchar(*_pos);
            _pos += _bpc;
        }
        return true;
    }

    for (i = 0; i < _xsize;) {
        if (_bpc == 2) {
            _pos++;
        }
        if (_pos >= _data.end()) {
            return false;
        }
        n = *_pos & 0x7f;
        if (!n) {
            break;
        }

        if (*_pos++ & 0x80) {
            for (; i < _xsize && _pos < _data.end() && n--; i++) {
                *dest++ = *_pos;
                _pos += _bpc;
            }
        } else {
            for (; i < _xsize && n--; i++) {
                *dest++ = *_pos;
            }

            _pos += _bpc;
        }
    }
    return i == _xsize;
}

bool SGIImagePrivate::readData(QImage &img)
{
    QRgb *c;
    quint32 *start = _starttab;
    QByteArray lguard(_xsize, 0);
    uchar *line = (uchar *)lguard.data();
    unsigned x;
    unsigned y;

    if (!_rle) {
        _pos = _data.begin();
    }

    for (y = 0; y < _ysize; y++) {
        if (_rle) {
            _pos = _data.begin() + *start++;
        }
        if (!getRow(line)) {
            return false;
        }
        c = reinterpret_cast<QRgb *>(img.scanLine(_ysize - y - 1));
        for (x = 0; x < _xsize; x++, c++) {
            *c = qRgb(line[x], line[x], line[x]);
        }
    }

    if (_zsize == 1) {
        return true;
    }

    if (_zsize != 2) {
        for (y = 0; y < _ysize; y++) {
            if (_rle) {
                _pos = _data.begin() + *start++;
            }
            if (!getRow(line)) {
                return false;
            }
            c = reinterpret_cast<QRgb *>(img.scanLine(_ysize - y - 1));
            for (x = 0; x < _xsize; x++, c++) {
                *c = qRgb(qRed(*c), line[x], line[x]);
            }
        }

        for (y = 0; y < _ysize; y++) {
            if (_rle) {
                _pos = _data.begin() + *start++;
            }
            if (!getRow(line)) {
                return false;
            }
            c = reinterpret_cast<QRgb *>(img.scanLine(_ysize - y - 1));
            for (x = 0; x < _xsize; x++, c++) {
                *c = qRgb(qRed(*c), qGreen(*c), line[x]);
            }
        }

        if (_zsize == 3) {
            return true;
        }
    }

    for (y = 0; y < _ysize; y++) {
        if (_rle) {
            _pos = _data.begin() + *start++;
        }
        if (!getRow(line)) {
            return false;
        }
        c = reinterpret_cast<QRgb *>(img.scanLine(_ysize - y - 1));
        for (x = 0; x < _xsize; x++, c++) {
            *c = qRgba(qRed(*c), qGreen(*c), qBlue(*c), line[x]);
        }
    }

    return true;
}

bool SGIImagePrivate::readImage(QImage &img)
{
    if (!readHeader() || !isSupported()) {
        return false;
    }

    if (_stream.atEnd()) {
        return false;
    }

    img = imageAlloc(size(), format());
    if (img.isNull()) {
        qCWarning(LOG_RGBPLUGIN) << "Failed to allocate image, invalid dimensions?" << QSize(_xsize, _ysize);
        return false;
    }

    if (_zsize > 4) {
        //         qCDebug(LOG_RGBPLUGIN) << "using first 4 of " << _zsize << " channels";
        // Only let this continue if it won't cause a int overflow later
        // this is most likely a broken file anyway
        if (_ysize > std::numeric_limits<int>::max() / _zsize) {
            return false;
        }
    }

    _numrows = _ysize * _zsize;

    if (_rle) {
        uint l;
        _starttab = new (std::nothrow) quint32[_numrows];
        if (_starttab == nullptr) {
            return false;
        }
        for (l = 0; !_stream.atEnd() && l < _numrows; l++) {
            _stream >> _starttab[l];
            _starttab[l] -= 512 + _numrows * 2 * sizeof(quint32);
            if (_stream.status() != QDataStream::Ok) {
                return false;
            }
        }
        for (; l < _numrows; l++) {
            _starttab[l] = 0;
        }

        _lengthtab = new (std::nothrow) quint32[_numrows];
        if (_lengthtab == nullptr) {
            return false;
        }
        for (l = 0; !_stream.atEnd() && l < _numrows; l++) {
            _stream >> _lengthtab[l];
            if (_stream.status() != QDataStream::Ok) {
                return false;
            }
        }
        for (; l < _numrows; l++) {
            _lengthtab[l] = 0;
        }
    }

    if (_stream.status() != QDataStream::Ok) {
        return false;
    }

    _data = _dev->readAll();

    // sanity check
    if (_rle) {
        for (uint o = 0; o < _numrows; o++) {
            // don't change to greater-or-equal!
            if (_starttab[o] + _lengthtab[o] > (uint)_data.size()) {
                //                 qCDebug(LOG_RGBPLUGIN) << "image corrupt (sanity check failed)";
                return false;
            }
        }
    }

    if (!readData(img)) {
        //         qCDebug(LOG_RGBPLUGIN) << "image corrupt (incomplete scanline)";
        return false;
    }

    return true;
}

///////////////////////////////////////////////////////////////////////////////

void RLEData::write(QDataStream &s)
{
    for (int i = 0; i < size(); i++) {
        s << at(i);
    }
}

bool RLEData::operator<(const RLEData &b) const
{
    uchar ac;
    uchar bc;
    for (int i = 0; i < qMin(size(), b.size()); i++) {
        ac = at(i);
        bc = b[i];
        if (ac != bc) {
            return ac < bc;
        }
    }
    return size() < b.size();
}

uint RLEMap::insert(const uchar *d, uint l)
{
    RLEData data = RLEData(d, l, _offset);
    Iterator it = find(data);
    if (it != end()) {
        return it.value();
    }

    _offset += l;
    return QMap<RLEData, uint>::insert(data, _counter++).value();
}

QList<const RLEData *> RLEMap::vector()
{
    QList<const RLEData *> v(size());
    for (Iterator it = begin(); it != end(); ++it) {
        v.replace(it.value(), &it.key());
    }

    return v;
}

uchar SGIImagePrivate::intensity(uchar c)
{
    if (c < _pixmin) {
        _pixmin = c;
    }
    if (c > _pixmax) {
        _pixmax = c;
    }
    return c;
}

uint SGIImagePrivate::compact(uchar *d, uchar *s)
{
    uchar *dest = d;
    uchar *src = s;
    uchar patt;
    uchar *t;
    uchar *end = s + _xsize;
    int i;
    int n;
    while (src < end) {
        for (n = 0, t = src; t + 2 < end && !(*t == t[1] && *t == t[2]); t++) {
            n++;
        }

        while (n) {
            i = n > 126 ? 126 : n;
            n -= i;
            *dest++ = 0x80 | i;
            while (i--) {
                *dest++ = *src++;
            }
        }

        if (src == end) {
            break;
        }

        patt = *src++;
        for (n = 1; src < end && *src == patt; src++) {
            n++;
        }

        while (n) {
            i = n > 126 ? 126 : n;
            n -= i;
            *dest++ = i;
            *dest++ = patt;
        }
    }
    *dest++ = 0;
    return dest - d;
}

bool SGIImagePrivate::scanData(const QImage &img, const QImage::Format &tfmt, const QColorSpace &tcs)
{
    quint32 *start = _starttab;
    QByteArray lineguard(_xsize * 2, 0);
    QByteArray bufguard(_xsize, 0);
    uchar *line = (uchar *)lineguard.data();
    uchar *buf = (uchar *)bufguard.data();
    const QRgb *c;
    unsigned x;
    unsigned y;
    uint len;

    ScanLineConverter scl(tfmt);
    scl.setTargetColorSpace(tcs);
    for (y = 0; y < _ysize; y++) {
        const int yPos = _ysize - y - 1; // scanline doesn't do any sanity checking
        if (yPos >= img.height()) {
            qCWarning(LOG_RGBPLUGIN) << "Failed to get scanline for" << yPos;
            return false;
        }

        c = reinterpret_cast<const QRgb *>(scl.convertedScanLine(img, yPos));

        for (x = 0; x < _xsize; x++) {
            buf[x] = intensity(qRed(*c++));
        }
        len = compact(line, buf);
        *start++ = _rlemap.insert(line, len);
    }

    if (_zsize == 1) {
        return true;
    }

    if (_zsize != 2) {
        for (y = 0; y < _ysize; y++) {
            const int yPos = _ysize - y - 1;
            if (yPos >= img.height()) {
                qCWarning(LOG_RGBPLUGIN) << "Failed to get scanline for" << yPos;
                return false;
            }

            c = reinterpret_cast<const QRgb *>(scl.convertedScanLine(img, yPos));
            for (x = 0; x < _xsize; x++) {
                buf[x] = intensity(qGreen(*c++));
            }
            len = compact(line, buf);
            *start++ = _rlemap.insert(line, len);
        }

        for (y = 0; y < _ysize; y++) {
            const int yPos = _ysize - y - 1;
            if (yPos >= img.height()) {
                qCWarning(LOG_RGBPLUGIN) << "Failed to get scanline for" << yPos;
                return false;
            }

            c = reinterpret_cast<const QRgb *>(scl.convertedScanLine(img, yPos));
            for (x = 0; x < _xsize; x++) {
                buf[x] = intensity(qBlue(*c++));
            }
            len = compact(line, buf);
            *start++ = _rlemap.insert(line, len);
        }

        if (_zsize == 3) {
            return true;
        }
    }

    for (y = 0; y < _ysize; y++) {
        const int yPos = _ysize - y - 1;
        if (yPos >= img.height()) {
            qCWarning(LOG_RGBPLUGIN) << "Failed to get scanline for" << yPos;
            return false;
        }

        c = reinterpret_cast<const QRgb *>(scl.convertedScanLine(img, yPos));
        for (x = 0; x < _xsize; x++) {
            buf[x] = intensity(qAlpha(*c++));
        }
        len = compact(line, buf);
        *start++ = _rlemap.insert(line, len);
    }

    return true;
}

bool SGIImagePrivate::isValid() const
{
    // File signature/magic number
    if (_magic != 0x01da) {
        return false;
    }
    // Compression, 0 = Uncompressed, 1 = RLE compressed
    if (_rle > 1) {
        return false;
    }
    // Bytes per pixel, 1 = 8 bit, 2 = 16 bit
    if (_bpc != 1 && _bpc != 2) {
        return false;
    }
    // Image dimension, 3 for RGBA image
    if (_dim < 1 || _dim > 3) {
        return false;
    }
    // Number channels in the image file, 4 for RGBA image
    if (_zsize < 1) {
        return false;
    }
    return true;
}

bool SGIImagePrivate::isSupported() const
{
    if (!isValid()) {
        return false;
    }
    if (_colormap != NORMAL) {
        return false; // only NORMAL supported
    }
    if (_dim == 1) {
        return false;
    }
    return true;
}

bool SGIImagePrivate::peekHeader(QIODevice *device)
{
    QDataStream ds(device->peek(512));
    return SGIImagePrivate::readHeader(ds, this) && isValid();
}

QSize SGIImagePrivate::size() const
{
    return QSize(_xsize, _ysize);
}

QImage::Format SGIImagePrivate::format() const
{
    if (_zsize == 2 || _zsize == 4) {
        return QImage::Format_ARGB32;
    }
    return QImage::Format_RGB32;
}

bool SGIImagePrivate::readHeader()
{
    return readHeader(_stream, this);
}

bool SGIImagePrivate::readHeader(QDataStream &ds, SGIImagePrivate *sgi)
{
    // magic
    ds >> sgi->_magic;

    // verbatim/rle
    ds >> sgi->_rle;

    // bytes per channel
    ds >> sgi->_bpc;

    // number of dimensions
    ds >> sgi->_dim;

    ds >> sgi->_xsize >> sgi->_ysize >> sgi->_zsize >> sgi->_pixmin >> sgi->_pixmax >> sgi->_unused32;

    // name
    ds.readRawData(sgi->_imagename, 80);
    sgi->_imagename[79] = '\0';

    ds >> sgi->_colormap;

    for (size_t i = 0; i < sizeof(_unused); i++) {
        ds >> sgi->_unused[i];
    }

    return ds.status() == QDataStream::Ok;
}

bool SGIImagePrivate::writeHeader()
{
    _stream << _magic;
    _stream << _rle << _bpc << _dim;
    _stream << _xsize << _ysize << _zsize;
    _stream << _pixmin << _pixmax;
    _stream << _unused32;

    for (int i = 0; i < 80; i++) {
        _imagename[i] = '\0';
    }
    _stream.writeRawData(_imagename, 80);

    _stream << _colormap;
    for (size_t i = 0; i < sizeof(_unused); i++) {
        _stream << _unused[i];
    }
    return _stream.status() == QDataStream::Ok;
}

bool SGIImagePrivate::writeRle()
{
    _rle = 1;
    //     qCDebug(LOG_RGBPLUGIN) << "writing RLE data";
    if (!writeHeader()) {
        return false;
    }

    uint i;

    // write start table
    for (i = 0; i < _numrows; i++) {
        _stream << quint32(_rlevector[_starttab[i]]->offset());
    }

    // write length table
    for (i = 0; i < _numrows; i++) {
        _stream << quint32(_rlevector[_starttab[i]]->size());
    }

    // write data
    for (i = 0; (int)i < _rlevector.size(); i++) {
        const_cast<RLEData *>(_rlevector[i])->write(_stream);
    }

    return _stream.status() == QDataStream::Ok;
}

bool SGIImagePrivate::writeVerbatim(const QImage &img, const QImage::Format &tfmt, const QColorSpace &tcs)
{
    _rle = 0;
    if (!writeHeader()) {
        return false;
    }

    const QRgb *c;
    unsigned x;
    unsigned y;

    ScanLineConverter scl(tfmt);
    scl.setTargetColorSpace(tcs);
    QByteArray ba(_xsize, char());
    for (y = 0; y < _ysize; y++) {
        c = reinterpret_cast<const QRgb *>(scl.convertedScanLine(img, _ysize - y - 1));
        for (x = 0; x < _xsize; x++) {
            ba[x] = char(qRed(*c++));
        }
        _stream.writeRawData(ba.data(), ba.size());
    }

    if (_zsize == 1) {
        return _stream.status() == QDataStream::Ok;
    }

    if (_zsize != 2) {
        for (y = 0; y < _ysize; y++) {
            c = reinterpret_cast<const QRgb *>(scl.convertedScanLine(img, _ysize - y - 1));
            for (x = 0; x < _xsize; x++) {
                ba[x] = char(qGreen(*c++));
            }
            _stream.writeRawData(ba.data(), ba.size());
        }

        for (y = 0; y < _ysize; y++) {
            c = reinterpret_cast<const QRgb *>(scl.convertedScanLine(img, _ysize - y - 1));
            for (x = 0; x < _xsize; x++) {
                ba[x] = char(qBlue(*c++));
            }
            _stream.writeRawData(ba.data(), ba.size());
        }

        if (_zsize == 3) {
            return _stream.status() == QDataStream::Ok;
        }
    }

    for (y = 0; y < _ysize; y++) {
        c = reinterpret_cast<const QRgb *>(scl.convertedScanLine(img, _ysize - y - 1));
        for (x = 0; x < _xsize; x++) {
            ba[x] = char(qAlpha(*c++));
        }
        _stream.writeRawData(ba.data(), ba.size());
    }

    return _stream.status() == QDataStream::Ok;
}

bool SGIImagePrivate::writeImage(const QImage &image)
{
    if (image.allGray()) {
        _dim = 2, _zsize = 1;
    } else {
        _dim = 3, _zsize = 3;
    }

    auto hasAlpha = image.hasAlphaChannel();
    if (hasAlpha) {
        _dim = 3, _zsize++;
    }

    auto tcs = QColorSpace();
    auto tfmt = image.format();
    auto cs = image.colorSpace();
    if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && tfmt == QImage::Format_CMYK8888) {
        tcs = QColorSpace(QColorSpace::SRgb);
        tfmt = QImage::Format_RGB32;
    } else if (hasAlpha && tfmt != QImage::Format_ARGB32) {
        tfmt = QImage::Format_ARGB32;
    } else if (!hasAlpha && tfmt != QImage::Format_RGB32) {
        tfmt = QImage::Format_RGB32;
    }

    const int w = image.width();
    const int h = image.height();

    if (w > 65535 || h > 65535) {
        return false;
    }

    _magic = 0x01da;
    _bpc = 1;
    _xsize = w;
    _ysize = h;
    _pixmin = ~0u;
    _pixmax = 0;
    _colormap = NORMAL;
    _numrows = _ysize * _zsize;
    _starttab = new (std::nothrow) quint32[_numrows];
    if (_starttab == nullptr) {
        return false;
    }
    _rlemap.setBaseOffset(512 + _numrows * 2 * sizeof(quint32));

    if (!scanData(image, tfmt, tcs)) {
        //         qCDebug(LOG_RGBPLUGIN) << "this can't happen";
        return false;
    }

    _rlevector = _rlemap.vector();

    long verbatim_size = _numrows * _xsize;
    long rle_size = _numrows * 2 * sizeof(quint32);
    for (int i = 0; i < _rlevector.size(); i++) {
        rle_size += _rlevector[i]->size();
    }

    if (verbatim_size <= rle_size) {
        return writeVerbatim(image, tfmt, tcs);
    }
    return writeRle();
}

///////////////////////////////////////////////////////////////////////////////

RGBHandler::RGBHandler()
    : QImageIOHandler()
    , d(new SGIImagePrivate)
{
}

bool RGBHandler::canRead() const
{
    if (canRead(device())) {
        setFormat("rgb");
        return true;
    }
    return false;
}

bool RGBHandler::read(QImage *outImage)
{
    d->setDevice(device());
    return d->readImage(*outImage);
}

bool RGBHandler::write(const QImage &image)
{
    d->setDevice(device());
    return d->writeImage(image);
}

bool RGBHandler::supportsOption(ImageOption option) const
{
    if (option == QImageIOHandler::Size) {
        return true;
    }
    if (option == QImageIOHandler::ImageFormat) {
        return true;
    }
    return false;
}

QVariant RGBHandler::option(ImageOption option) const
{
    QVariant v;

    if (option == QImageIOHandler::Size) {
        auto &&sgi = d;
        if (sgi->isSupported()) {
            v = QVariant::fromValue(sgi->size());
        } else if (auto dev = device()) {
            if (d->peekHeader(dev) && sgi->isSupported()) {
                v = QVariant::fromValue(sgi->size());
            }
        }
    }

    if (option == QImageIOHandler::ImageFormat) {
        auto &&sgi = d;
        if (sgi->isSupported()) {
            v = QVariant::fromValue(sgi->format());
        } else if (auto dev = device()) {
            if (d->peekHeader(dev) && sgi->isSupported()) {
                v = QVariant::fromValue(sgi->format());
            }
        }
    }

    return v;
}

bool RGBHandler::canRead(QIODevice *device)
{
    if (!device) {
        qCWarning(LOG_RGBPLUGIN) << "RGBHandler::canRead() called with no device";
        return false;
    }

    SGIImagePrivate sgi;
    return sgi.peekHeader(device) && sgi.isSupported();
}

///////////////////////////////////////////////////////////////////////////////

QImageIOPlugin::Capabilities RGBPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
    if (format == "rgb" || format == "rgba" || format == "bw" || format == "sgi") {
        return Capabilities(CanRead | CanWrite);
    }
    if (!format.isEmpty()) {
        return {};
    }
    if (!device->isOpen()) {
        return {};
    }

    Capabilities cap;
    if (device->isReadable() && RGBHandler::canRead(device)) {
        cap |= CanRead;
    }
    if (device->isWritable()) {
        cap |= CanWrite;
    }
    return cap;
}

QImageIOHandler *RGBPlugin::create(QIODevice *device, const QByteArray &format) const
{
    QImageIOHandler *handler = new RGBHandler;
    handler->setDevice(device);
    handler->setFormat(format);
    return handler;
}

#include "moc_rgb_p.cpp"
