/*
    This file is part of the KDE project
    SPDX-FileCopyrightText: 2003 Dominik Seichter <domseichter@web.de>
    SPDX-FileCopyrightText: 2004 Ignacio Castaño <castano@ludicon.com>
    SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>

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

/* this code supports:
 * reading:
 *     uncompressed and run length encoded indexed, grey and color tga files.
 *     image types 1, 2, 3, 9, 10 and 11.
 *     only RGB color maps with no more than 256 colors.
 *     pixel formats 8, 15, 16, 24 and 32.
 * writing:
 *     uncompressed rgb color tga files
 *     uncompressed grayscale tga files
 *     uncompressed indexed tga files
 */

#include "microexif_p.h"
#include "scanlineconverter_p.h"
#include "tga_p.h"
#include "util_p.h"

#include <assert.h>

#include <QColorSpace>
#include <QDataStream>
#include <QDateTime>
#include <QImage>
#include <QLoggingCategory>

typedef quint32 uint;
typedef quint16 ushort;
typedef quint8 uchar;

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

#ifndef TGA_V2E_AS_DEFAULT
/*
 * Uncomment to change the default version of the plugin to `TGAv2E`.
 */
// #define TGA_V2E_AS_DEFAULT
#endif // TGA_V2E_AS_DEFAULT

namespace // Private.
{
// Header format of saved files.
enum TGAType {
    TGA_TYPE_INDEXED = 1,
    TGA_TYPE_RGB = 2,
    TGA_TYPE_GREY = 3,
    TGA_TYPE_RLE_INDEXED = 9,
    TGA_TYPE_RLE_RGB = 10,
    TGA_TYPE_RLE_GREY = 11,
};

#define TGA_INTERLEAVE_MASK 0xc0
#define TGA_INTERLEAVE_NONE 0x00
#define TGA_INTERLEAVE_2WAY 0x40
#define TGA_INTERLEAVE_4WAY 0x80

#define TGA_ORIGIN_MASK 0x30
#define TGA_ORIGIN_LEFT 0x00
#define TGA_ORIGIN_RIGHT 0x10
#define TGA_ORIGIN_LOWER 0x00
#define TGA_ORIGIN_UPPER 0x20

/*
 * Each TAG is a SHORT value in the range of 0 to 65535. Values from 0 - 32767 are available
 * for developer use, while values from 32768 - 65535 are reserved for Truevision.
 * Truevision will maintain a list of tags assigned to companies.
 * In any case, there's no public "list of tag" and Truevision no longer exists.
 */
#define TGA_EXIF_TAGID 0x7001 // Exif data preceded by "eXif" string
#define TGA_XMPP_TAGID 0x7002 // Xmp packet preceded by "xMPP" string
#define TGA_ICCP_TAGID 0x7003 // Icc profile preceded by "iCCP" string

/** Tga Header. */
struct TgaHeader {
    uchar id_length = 0;
    uchar colormap_type = 0;
    uchar image_type = 0;
    ushort colormap_index = 0;
    ushort colormap_length = 0;
    uchar colormap_size = 0;
    ushort x_origin = 0;
    ushort y_origin = 0;
    ushort width = 0;
    ushort height = 0;
    uchar pixel_size = 0;
    uchar flags = 0;

    enum {
        SIZE = 18,
    }; // const static int SIZE = 18;
};

/** Tga 2.0 Footer */
struct TgaFooter {
    TgaFooter()
        : extensionOffset(0)
        , developerOffset(0)
    {
        std::memcpy(signature, "TRUEVISION-XFILE.\0", 18);
    }
    bool isValid() const
    {
        return std::memcmp(signature, "TRUEVISION-XFILE.\0", 18) == 0;
    }

    quint32 extensionOffset; // Extension Area Offset
    quint32 developerOffset; // Developer Directory Offset
    char signature[18]; // TGA Signature
};

/** Tga 2.0 extension area */
struct TgaExtension {
    enum AttributeType : quint16 {
        NoAlpha = 0, // no Alpha data included (bits 3-0 of TgaHeader::flags should also be set to zero).
        IgnoreAlpha = 1, // undefined data in the Alpha field, can be ignored
        RetainAlpha = 2, // undefined data in the Alpha field, but should be retained
        Alpha = 3, // useful Alpha channel data is present
        PremultipliedAlpha = 4 // pre-multiplied Alpha (see description below)
    };

    TgaExtension()
    {
        std::memset(this, 0, sizeof(TgaExtension));
        size = 495; // TGA 2.0 specs

        // If you do not use Software Version field, set the SHORT to binary
        // zero, and the BYTE to a space (' ').
        versionLetter = 0x20;
    }

    bool isValid() const
    {
        return size == 495;
    }

    void setDateTime(const QDateTime &dt)
    {
        if (dt.isValid()) {
            auto date = dt.date();
            stampMonth = date.month();
            stampDay = date.day();
            stampYear = date.year();
            auto time = dt.time();
            stampHour = time.hour();
            stampMinute = time.minute();
            stampSecond = time.second();
        }
    }
    QDateTime dateTime() const
    {
        auto date = QDate(stampYear, stampMonth, stampDay);
        auto time = QTime(stampHour, stampMinute, stampSecond);
        if (!date.isValid() || !time.isValid())
            return {};
        return QDateTime(date, time);
    }

    void setAuthor(const QString &str)
    {
        auto ba = str.toLatin1();
        std::memcpy(authorName, ba.data(), std::min(sizeof(authorName) - 1, size_t(ba.size())));
    }
    QString author() const
    {
        if (authorName[sizeof(authorName) - 1] != char(0))
            return {};
        return QString::fromLatin1(authorName);
    }

    void setComment(const QString &str)
    {
        auto ba = str.toLatin1();
        std::memcpy(authorComment, ba.data(), std::min(sizeof(authorComment) - 1, size_t(ba.size())));
    }
    QString comment() const
    {
        if (authorComment[sizeof(authorComment) - 1] != char(0))
            return {};
        return QString::fromLatin1(authorComment);
    }

    void setSoftware(const QString &str)
    {
        auto ba = str.toLatin1();
        std::memcpy(softwareId, ba.data(), std::min(sizeof(softwareId) - 1, size_t(ba.size())));
    }
    QString software() const
    {
        if (softwareId[sizeof(softwareId) - 1] != char(0))
            return {};
        return QString::fromLatin1(softwareId);
    }

    quint16 size; // Extension Size
    char authorName[41]; // Author Name
    char authorComment[324]; // Author Comment
    quint16 stampMonth; // Date/Time Stamp: Month
    quint16 stampDay; // Date/Time Stamp: Day
    quint16 stampYear; // Date/Time Stamp: Year
    quint16 stampHour; // Date/Time Stamp: Hour
    quint16 stampMinute; // Date/Time Stamp: Minute
    quint16 stampSecond; // Date/Time Stamp: Second
    char jobName[41]; // Job Name/ID
    quint16 jobHour; // Job Time: Hours
    quint16 jobMinute; // Job Time: Minutes
    quint16 jobSecond; // Job Time: Seconds
    char softwareId[41]; // Software ID
    quint16 versionNumber; // Software Version Number
    quint8 versionLetter; // Software Version Letter
    quint32 keyColor; // Key Color
    quint16 pixelNumerator; // Pixel Aspect Ratio
    quint16 pixelDenominator; // Pixel Aspect Ratio
    quint16 gammaNumerator; // Gamma Value
    quint16 gammaDenominator; // Gamma Value
    quint32 colorOffset; // Color Correction Offset
    quint32 stampOffset; // Postage Stamp Offset
    quint32 scanOffset; // Scan-Line Table Offset
    quint8 attributesType; // Attributes Types
};

struct TgaDeveloperDirectory {
    struct Field {
        quint16 tagId;
        quint32 offset;
        quint32 size;
    };

    bool isEmpty() const
    {
        return fields.isEmpty();
    }

    QList<Field> fields;
};

static QDataStream &operator>>(QDataStream &s, TgaHeader &head)
{
    s >> head.id_length;
    s >> head.colormap_type;
    s >> head.image_type;
    s >> head.colormap_index;
    s >> head.colormap_length;
    s >> head.colormap_size;
    s >> head.x_origin;
    s >> head.y_origin;
    s >> head.width;
    s >> head.height;
    s >> head.pixel_size;
    s >> head.flags;
    return s;
}

static QDataStream &operator>>(QDataStream &s, TgaFooter &footer)
{
    s >> footer.extensionOffset;
    s >> footer.developerOffset;
    s.readRawData(footer.signature, sizeof(footer.signature));
    return s;
}

static QDataStream &operator<<(QDataStream &s, const TgaFooter &footer)
{
    s << footer.extensionOffset;
    s << footer.developerOffset;
    s.writeRawData(footer.signature, sizeof(footer.signature));
    return s;
}

static QDataStream &operator>>(QDataStream &s, TgaDeveloperDirectory &dir)
{
    quint16 n;
    s >> n;
    for (auto i = n; i > 0; --i) {
        TgaDeveloperDirectory::Field f;
        s >> f.tagId;
        s >> f.offset;
        s >> f.size;
        dir.fields << f;
    }
    return s;
}

static QDataStream &operator<<(QDataStream &s, const TgaDeveloperDirectory &dir)
{
    s << quint16(dir.fields.size());
    for (auto &&f : dir.fields) {
        s << f.tagId;
        s << f.offset;
        s << f.size;
    }
    return s;
}

static QDataStream &operator>>(QDataStream &s, TgaExtension &ext)
{
    s >> ext.size;
    s.readRawData(ext.authorName, sizeof(ext.authorName));
    s.readRawData(ext.authorComment, sizeof(ext.authorComment));
    s >> ext.stampMonth;
    s >> ext.stampDay;
    s >> ext.stampYear;
    s >> ext.stampHour;
    s >> ext.stampMinute;
    s >> ext.stampSecond;
    s.readRawData(ext.jobName, sizeof(ext.jobName));
    s >> ext.jobHour;
    s >> ext.jobMinute;
    s >> ext.jobSecond;
    s.readRawData(ext.softwareId, sizeof(ext.softwareId));
    s >> ext.versionNumber;
    s >> ext.versionLetter;
    s >> ext.keyColor;
    s >> ext.pixelNumerator;
    s >> ext.pixelDenominator;
    s >> ext.gammaNumerator;
    s >> ext.gammaDenominator;
    s >> ext.colorOffset;
    s >> ext.stampOffset;
    s >> ext.scanOffset;
    s >> ext.attributesType;
    return s;
}

static QDataStream &operator<<(QDataStream &s, const TgaExtension &ext)
{
    s << ext.size;
    s.writeRawData(ext.authorName, sizeof(ext.authorName));
    s.writeRawData(ext.authorComment, sizeof(ext.authorComment));
    s << ext.stampMonth;
    s << ext.stampDay;
    s << ext.stampYear;
    s << ext.stampHour;
    s << ext.stampMinute;
    s << ext.stampSecond;
    s.writeRawData(ext.jobName, sizeof(ext.jobName));
    s << ext.jobHour;
    s << ext.jobMinute;
    s << ext.jobSecond;
    s.writeRawData(ext.softwareId, sizeof(ext.softwareId));
    s << ext.versionNumber;
    s << ext.versionLetter;
    s << ext.keyColor;
    s << ext.pixelNumerator;
    s << ext.pixelDenominator;
    s << ext.gammaNumerator;
    s << ext.gammaDenominator;
    s << ext.colorOffset;
    s << ext.stampOffset;
    s << ext.scanOffset;
    s << ext.attributesType;
    return s;
}

static bool IsSupported(const TgaHeader &head)
{
    if (head.image_type != TGA_TYPE_INDEXED && head.image_type != TGA_TYPE_RGB && head.image_type != TGA_TYPE_GREY && head.image_type != TGA_TYPE_RLE_INDEXED
        && head.image_type != TGA_TYPE_RLE_RGB && head.image_type != TGA_TYPE_RLE_GREY) {
        return false;
    }
    if (head.image_type == TGA_TYPE_INDEXED || head.image_type == TGA_TYPE_RLE_INDEXED) {
        // GIMP saves TGAs with palette size of 257 (but 256 used) so, I need to check the pixel size only.
        if (head.pixel_size > 8 || head.colormap_type != 1) {
            return false;
        }
        if (head.colormap_size != 15 && head.colormap_size != 16 && head.colormap_size != 24 && head.colormap_size != 32) {
            return false;
        }
    }
    if (head.image_type == TGA_TYPE_RGB || head.image_type == TGA_TYPE_GREY || head.image_type == TGA_TYPE_RLE_RGB || head.image_type == TGA_TYPE_RLE_GREY) {
        if (head.colormap_type != 0) {
            return false;
        }
    }
    if (head.width == 0 || head.height == 0) {
        return false;
    }
    if (head.pixel_size != 8 && head.pixel_size != 15 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) {
        return false;
    }
    // If the colormap_type field is set to zero, indicating that no color map exists, then colormap_index and colormap_length should be set to zero.
    if (head.colormap_type == 0 && (head.colormap_index != 0 || head.colormap_length != 0)) {
        return false;
    }

    return true;
}

/*!
 * \brief imageId
 * Create the TGA imageId from the image TITLE metadata
 */
static QByteArray imageId(const QImage &img)
{
    auto ba = img.text(QStringLiteral(META_KEY_TITLE)).trimmed().toLatin1();
    if (ba.size() > 255)
        ba = ba.left(255);
    return ba;
}

struct TgaHeaderInfo {
    bool rle;
    bool pal;
    bool rgb;
    bool grey;

    TgaHeaderInfo(const TgaHeader &tga)
        : rle(false)
        , pal(false)
        , rgb(false)
        , grey(false)
    {
        switch (tga.image_type) {
        case TGA_TYPE_RLE_INDEXED:
            rle = true;
            Q_FALLTHROUGH();
        // no break is intended!
        case TGA_TYPE_INDEXED:
            pal = true;
            break;

        case TGA_TYPE_RLE_RGB:
            rle = true;
            Q_FALLTHROUGH();
        // no break is intended!
        case TGA_TYPE_RGB:
            rgb = true;
            break;

        case TGA_TYPE_RLE_GREY:
            rle = true;
            Q_FALLTHROUGH();
        // no break is intended!
        case TGA_TYPE_GREY:
            grey = true;
            break;

        default:
            // Error, unknown image type.
            break;
        }
    }
};

static QImage::Format imageFormat(const TgaHeader &head)
{
    auto format = QImage::Format_Invalid;
    if (IsSupported(head)) {
        TgaHeaderInfo info(head);

        // Bits 0-3 are the numbers of alpha bits (can be zero!)
        const int numAlphaBits = head.flags & 0xf;
        // However alpha should exists only in the 32 bit format.
        if ((head.pixel_size == 32) && (numAlphaBits)) {
            if (numAlphaBits <= 8) {
                format = QImage::Format_ARGB32;
            }
        // Anyway, GIMP also saves gray images with alpha in TGA format
        } else if ((info.grey) && (head.pixel_size == 16) && (numAlphaBits)) {
            if (numAlphaBits == 8) {
                format = QImage::Format_ARGB32;
            }
        } else if (info.grey) {
            format = QImage::Format_Grayscale8;
        } else if (info.pal) {
            format = QImage::Format_Indexed8;
        } else if (info.rgb && (head.pixel_size == 15 || head.pixel_size == 16)) {
            format = QImage::Format_RGB555;
        } else {
            format = QImage::Format_RGB32;
        }
    }
    return format;
}

/*!
 * \brief peekHeader
 * Reads the header but does not change the position in the device.
 */
static bool peekHeader(QIODevice *device, TgaHeader &header)
{
    auto head = device->peek(TgaHeader::SIZE);
    if (head.size() < TgaHeader::SIZE) {
        return false;
    }
    QDataStream stream(head);
    stream.setByteOrder(QDataStream::LittleEndian);
    stream >> header;
    return true;
}

/*!
 * \brief readTgaLine
 * Read a scan line from the raw data.
 * \param dev The current device.
 * \param pixel_size The number of bytes per pixel.
 * \param size The size of the uncompressed TGA raw line
 * \param rle True if the stream is RLE compressed, otherwise false.
 * \param cache The cache buffer used to store data (only used when the stream is RLE).
 * \return The uncompressed raw data of a line or an empty array on error.
 */
static QByteArray readTgaLine(QIODevice *dev, qint32 pixel_size, qint32 size, bool rle, QByteArray &cache)
{
    // uncompressed stream
    if (!rle) {
        auto ba = dev->read(size);
        if (ba.size() != size)
            ba.clear();
        return ba;
    }

    // RLE compressed stream
    if (cache.size() < qsizetype(size)) {
        // Decode image.
        qint64 num = size;

        while (num > 0) {
            if (dev->atEnd()) {
                break;
            }

            // Get packet header.
            char cc;
            if (dev->read(&cc, 1) != 1) {
                cache.clear();
                break;
            }
            auto c = uchar(cc);

            uint count = (c & 0x7f) + 1;
            QByteArray tmp(count * pixel_size, char());
            auto dst = tmp.data();
            num -= count * pixel_size;

            if (c & 0x80) { // RLE pixels.
                assert(pixel_size <= 8);
                char pixel[8];
                const int dataRead = dev->read(pixel, pixel_size);
                if (dataRead < (int)pixel_size) {
                    memset(&pixel[dataRead], 0, pixel_size - dataRead);
                }
                do {
                    memcpy(dst, pixel, pixel_size);
                    dst += pixel_size;
                } while (--count);
            } else { // Raw pixels.
                count *= pixel_size;
                const int dataRead = dev->read(dst, count);
                if (dataRead < 0) {
                    cache.clear();
                    break;
                }

                if ((uint)dataRead < count) {
                    const size_t toCopy = count - dataRead;
                    memset(&dst[dataRead], 0, toCopy);
                }
                dst += count;
            }

            cache.append(tmp);
        }
    }

    auto data = cache.left(size);
    cache.remove(0, size);
    if (data.size() != size)
        data.clear();
    return data;
}

inline QRgb rgb555ToRgb(char c0, char c1)
{
    // c0 = GGGBBBBB
    // c1 = IRRRRRGG (I = interrupt control of VDA(D) -> ignore it)
    return qRgb(int((c1 >> 2) & 0x1F) * 255 / 31, int(((c1 & 3) << 3) | ((c0 >> 5) & 7)) * 255 / 31, int(c0 & 0x1F) * 255 / 31);
}

static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img)
{
    img = imageAlloc(tga.width, tga.height, imageFormat(tga));
    if (img.isNull()) {
        qCWarning(LOG_TGAPLUGIN) << "LoadTGA: Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height);
        return false;
    }

    TgaHeaderInfo info(tga);

    const int numAlphaBits = qBound(0, tga.flags & 0xf, 8);
    bool hasAlpha = img.hasAlphaChannel() && numAlphaBits > 0;
    qint32 pixel_size = (tga.pixel_size == 15 ? 16 : tga.pixel_size) / 8;
    qint32 line_size = qint32(tga.width) * pixel_size;
    qint64 size = qint64(tga.height) * line_size;
    if (size < 1) {
        //          qCDebug(LOG_TGAPLUGIN) << "This TGA file is broken with size " << size;
        return false;
    }

    // Read palette.
    if (info.pal) {
        QList<QRgb> colorTable;
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
        colorTable.resize(tga.colormap_length);
#else
        colorTable.resizeForOverwrite(tga.colormap_length);
#endif

        if (tga.colormap_size == 32) {
            char data[4]; // BGRA
            for (QRgb &rgb : colorTable) {
                const auto dataRead = dev->read(data, 4);
                if (dataRead < 4) {
                    return false;
                }
                // BGRA.
                rgb = qRgba(data[2], data[1], data[0], data[3]);
            }
        } else if (tga.colormap_size == 24) {
            char data[3]; // BGR
            for (QRgb &rgb : colorTable) {
                const auto dataRead = dev->read(data, 3);
                if (dataRead < 3) {
                    return false;
                }
                // BGR.
                rgb = qRgb(data[2], data[1], data[0]);
            }
        } else if (tga.colormap_size == 16 || tga.colormap_size == 15) {
            char data[2];
            for (QRgb &rgb : colorTable) {
                const auto dataRead = dev->read(data, 2);
                if (dataRead < 2) {
                    return false;
                }
                rgb = rgb555ToRgb(data[0], data[1]);
            }
        } else {
            return false;
        }

        img.setColorTable(colorTable);
    }

    // Convert image to internal format.
    bool valid = true;
    int y_start = tga.height - 1;
    int y_step = -1;
    int y_end = -1;
    if (tga.flags & TGA_ORIGIN_UPPER) {
        y_start = 0;
        y_step = 1;
        y_end = tga.height;
    }
    int x_start = 0;
    int x_step = 1;
    int x_end = tga.width;
    if (tga.flags & TGA_ORIGIN_RIGHT) {
        x_start = tga.width - 1;
        x_step = -1;
        x_end = -1;
    }

    QByteArray cache;
    for (int y = y_start; y != y_end; y += y_step) {
        auto tgaLine = readTgaLine(dev, pixel_size, line_size, info.rle, cache);
        if (tgaLine.size() != qsizetype(line_size)) {
            qCWarning(LOG_TGAPLUGIN) << "LoadTGA: Error while decoding a TGA raw line";
            valid = false;
            break;
        }
        auto src = tgaLine.data();
        if (info.pal && img.depth() == 8) {
            // Paletted.
            auto scanline = img.scanLine(y);
            for (int x = x_start; x != x_end; x += x_step) {
                uchar idx = *src++;
                if (Q_UNLIKELY(idx >= tga.colormap_length)) {
                    valid = false;
                    break;
                }
                scanline[x] = idx;
            }
        } else if (info.grey) {
            if (tga.pixel_size == 16 && img.depth() == 32) { // Greyscale with alpha.
                auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
                for (int x = x_start; x != x_end; x += x_step) {
                    scanline[x] = qRgba(*src, *src, *src, *(src + 1));
                    src += 2;
                }
            } else if (tga.pixel_size == 8 && img.depth() == 8) { // Greyscale.
                auto scanline = img.scanLine(y);
                for (int x = x_start; x != x_end; x += x_step) {
                    scanline[x] = *src;
                    src++;
                }
            } else {
                valid = false;
                break;
            }
        } else {
            // True Color.
            if ((tga.pixel_size == 15 || tga.pixel_size == 16) && img.depth() == 16) {
                auto scanline = reinterpret_cast<quint16 *>(img.scanLine(y));
                for (int x = x_start; x != x_end; x += x_step) {
                    scanline[x] = ((quint16(src[1] & 0x7f) << 8) | quint8(src[0]));
                    src += 2;
                }
            } else if (tga.pixel_size == 24 && img.depth() == 32) {
                auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
                for (int x = x_start; x != x_end; x += x_step) {
                    scanline[x] = qRgb(src[2], src[1], src[0]);
                    src += 3;
                }
            } else if (tga.pixel_size == 32 && img.depth() == 32) {
                auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
                auto div = (1 << numAlphaBits) - 1;
                if (div == 0)
                    hasAlpha = false;
                for (int x = x_start; x != x_end; x += x_step) {
                    const int alpha = hasAlpha ? int((src[3]) << (8 - numAlphaBits)) * 255 / div : 255;
                    scanline[x] = qRgba(src[2], src[1], src[0], alpha);
                    src += 4;
                }
            } else {
                valid = false;
                break;
            }
        }
    }

    if (!cache.isEmpty() && valid) {
        qCDebug(LOG_TGAPLUGIN) << "LoadTGA: Found unused image data";
    }

    return valid;
}

} // namespace

class TGAHandlerPrivate
{
public:
    TGAHandlerPrivate()
#ifdef TGA_V2E_AS_DEFAULT
        : m_subType(subTypeTGA_V2E())
#else
        : m_subType(subTypeTGA_V2S())
#endif
    {
    }
    ~TGAHandlerPrivate() {}

    static QByteArray subTypeTGA_V1()
    {
        return QByteArrayLiteral("TGAv1");
    }
    static QByteArray subTypeTGA_V2S()
    {
        return QByteArrayLiteral("TGAv2");
    }
    static QByteArray subTypeTGA_V2E()
    {
        return QByteArrayLiteral("TGAv2E");
    }

    TgaHeader m_header;

    QByteArray m_subType;
};

TGAHandler::TGAHandler()
    : QImageIOHandler()
    , d(new TGAHandlerPrivate)
{
}

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

bool TGAHandler::read(QImage *outImage)
{
    // qCDebug(LOG_TGAPLUGIN) << "Loading TGA file!";

    auto dev = device();
    auto&& tga = d->m_header;
    if (!peekHeader(dev, tga) || !IsSupported(tga)) {
        //         qCDebug(LOG_TGAPLUGIN) << "This TGA file is not valid.";
        return false;
    }

    QByteArray imageId;
    if (dev->isSequential()) {
        auto tmp = dev->read(TgaHeader::SIZE);
        if (tmp.size() != TgaHeader::SIZE)
            return false;
    } else {
        if (!dev->seek(TgaHeader::SIZE))
            return false;
    }
    if (tga.id_length > 0) {
        imageId = dev->read(tga.id_length);
    }

    // Check image file format.
    if (dev->atEnd()) {
        //         qCDebug(LOG_TGAPLUGIN) << "This TGA file is not valid.";
        return false;
    }

    QImage img;
    if (!LoadTGA(dev, tga, img)) {
        //         qCDebug(LOG_TGAPLUGIN) << "Error loading TGA file.";
        return false;
    } else if (!imageId.isEmpty()) {
        img.setText(QStringLiteral(META_KEY_TITLE), QString::fromLatin1(imageId));
    }
    if (!readMetadata(img)) {
        qCDebug(LOG_TGAPLUGIN) << "read: error while reading metadata";
    }

    *outImage = img;
    return true;
}

bool TGAHandler::write(const QImage &image)
{
    auto ok = false;
    if (image.format() == QImage::Format_Indexed8) {
        ok = writeIndexed(image);
    } else if (image.format() == QImage::Format_Grayscale8 || image.format() == QImage::Format_Grayscale16) {
        ok = writeGrayscale(image);
    } else if (image.format() == QImage::Format_RGB555 || image.format() == QImage::Format_RGB16 || image.format() == QImage::Format_RGB444) {
        ok = writeRGB555(image);
    } else {
        ok = writeRGBA(image);
    }
    return (ok && writeMetadata(image));
}

bool TGAHandler::writeIndexed(const QImage &image)
{
    auto dev = device();
    { // write header and palette
        QDataStream s(dev);
        s.setByteOrder(QDataStream::LittleEndian);

        auto ct = image.colorTable();
        auto iid = imageId(image);
        s << quint8(iid.size()); // ID Length
        s << quint8(1); // Color Map Type
        s << quint8(TGA_TYPE_INDEXED); // Image Type
        s << quint16(0); // First Entry Index
        s << quint16(ct.size()); // Color Map Length
        s << quint8(32); // Color map Entry Size
        s << quint16(0); // X-origin of Image
        s << quint16(0); // Y-origin of Image

        s << quint16(image.width()); // Image Width
        s << quint16(image.height()); // Image Height
        s << quint8(8); // Pixel Depth
        s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor

        if (!iid.isEmpty())
            s.writeRawData(iid.data(), iid.size());

        for (auto &&rgb : ct) {
            s << quint8(qBlue(rgb));
            s << quint8(qGreen(rgb));
            s << quint8(qRed(rgb));
            s << quint8(qAlpha(rgb));
        }

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

    for (int y = 0, h = image.height(), w = image.width(); y < h; y++) {
        auto ptr = reinterpret_cast<const char *>(image.constScanLine(y));
        if (dev->write(ptr, w) != w) {
            return false;
        }
    }

    return true;
}

bool TGAHandler::writeGrayscale(const QImage &image)
{
    auto dev = device();
    { // write header
        QDataStream s(dev);
        s.setByteOrder(QDataStream::LittleEndian);

        auto iid = imageId(image);
        s << quint8(iid.size()); // ID Length
        s << quint8(0); // Color Map Type
        s << quint8(TGA_TYPE_GREY); // Image Type
        s << quint16(0); // First Entry Index
        s << quint16(0); // Color Map Length
        s << quint8(0); // Color map Entry Size
        s << quint16(0); // X-origin of Image
        s << quint16(0); // Y-origin of Image

        s << quint16(image.width()); // Image Width
        s << quint16(image.height()); // Image Height
        s << quint8(8); // Pixel Depth
        s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor

        if (!iid.isEmpty())
            s.writeRawData(iid.data(), iid.size());

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

    ScanLineConverter scl(QImage::Format_Grayscale8);
    for (int y = 0, h = image.height(), w = image.width(); y < h; y++) {
        auto ptr = reinterpret_cast<const char *>(scl.convertedScanLine(image, y));
        if (dev->write(ptr, w) != w) {
            return false;
        }
    }

    return true;
}

bool TGAHandler::writeRGB555(const QImage &image)
{
    auto dev = device();
    { // write header
        QDataStream s(dev);
        s.setByteOrder(QDataStream::LittleEndian);

        auto iid = imageId(image);
        for (char c : {int(iid.size()), 0, int(TGA_TYPE_RGB), 0, 0, 0, 0, 0, 0, 0, 0, 0}) {
            s << c;
        }
        s << quint16(image.width()); // width
        s << quint16(image.height()); // height
        s << quint8(16); // depth
        s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT);

        if (!iid.isEmpty())
            s.writeRawData(iid.data(), iid.size());

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

    ScanLineConverter scl(QImage::Format_RGB555);
    QByteArray ba(image.width() * 2, char());
    for (int y = 0, h = image.height(); y < h; y++) {
        auto ptr = reinterpret_cast<const quint16 *>(scl.convertedScanLine(image, y));
        for (int x = 0, w = image.width(); x < w; x++) {
            auto color = *(ptr + x);
            ba[x * 2] = char(color);
            ba[x * 2 + 1] = char(color >> 8);
        }
        if (dev->write(ba.data(), ba.size()) != qint64(ba.size())) {
            return false;
        }
    }

    return true;
}

bool TGAHandler::writeRGBA(const QImage &image)
{
    auto format = image.format();
    const bool hasAlpha = image.hasAlphaChannel();
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
    auto cs = image.colorSpace();
    auto tcs = QColorSpace();
    if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && image.format() == QImage::Format_CMYK8888) {
        format = QImage::Format_RGB32;
        tcs = QColorSpace(QColorSpace::SRgb);
    } else if (hasAlpha && image.format() != QImage::Format_ARGB32) {
#else
    if (hasAlpha && image.format() != QImage::Format_ARGB32) {
#endif
        format = QImage::Format_ARGB32;
    } else if (!hasAlpha && image.format() != QImage::Format_RGB32) {
        format = QImage::Format_RGB32;
    }

    auto dev = device();
    { // write header
        QDataStream s(dev);
        s.setByteOrder(QDataStream::LittleEndian);

        const quint8 originTopLeft = TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT; // 0x20
        const quint8 alphaChannel8Bits = 0x08;

        auto iid = imageId(image);
        for (char c : {int(iid.size()), 0, int(TGA_TYPE_RGB), 0, 0, 0, 0, 0, 0, 0, 0, 0}) {
            s << c;
        }
        s << quint16(image.width()); // width
        s << quint16(image.height()); // height
        s << quint8(hasAlpha ? 32 : 24); // depth (24 bit RGB + 8 bit alpha)
        s << quint8(hasAlpha ? originTopLeft + alphaChannel8Bits : originTopLeft); // top left image (0x20) + 8 bit alpha (0x8)

        if (!iid.isEmpty())
            s.writeRawData(iid.data(), iid.size());

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

    ScanLineConverter scl(format);
    if (tcs.isValid()) {
        scl.setTargetColorSpace(tcs);
    }
    auto mul = hasAlpha ? 4 : 3;
    QByteArray ba(image.width() * mul, char());
    for (int y = 0, h = image.height(); y < h; y++) {
        auto ptr = reinterpret_cast<const QRgb *>(scl.convertedScanLine(image, y));
        for (int x = 0, w = image.width(); x < w; x++) {
            auto color = *(ptr + x);
            auto xmul = x * mul;
            ba[xmul] = char(qBlue(color));
            ba[xmul + 1] = char(qGreen(color));
            ba[xmul + 2] = char(qRed(color));
            if (hasAlpha) {
                ba[xmul + 3] = char(qAlpha(color));
            }
        }
        if (dev->write(ba.data(), ba.size()) != qint64(ba.size())) {
            return false;
        }
    }

    return true;
}

bool TGAHandler::writeMetadata(const QImage &image)
{
    if (d->m_subType == TGAHandlerPrivate::subTypeTGA_V1()) {
        return true; // TGA V1 does not have these data
    }

    auto dev = device();
    if (dev == nullptr) {
        return false;
    }
    if (dev->isSequential()) {
        qCInfo(LOG_TGAPLUGIN) << "writeMetadata: unable to save metadata on a sequential device";
        return true;
    }

    QDataStream s(dev);
    s.setByteOrder(QDataStream::LittleEndian);

    // TGA 2.0 footer
    TgaFooter foot;

    // 32-bit overflow check (rough check)
    // I need at least 495 (extension) + 26 (footer) bytes -> 1024 bytes.
    // for the development area I roughly estimate 4096 KiB (profile, exif and xmp) they should always be less.
    auto reqBytes = qint64(d->m_subType == TGAHandlerPrivate::subTypeTGA_V2E() ? 4096 * 1024 : 1024);
    if (dev->pos() > std::numeric_limits<quint32>::max() - reqBytes) {
        qCInfo(LOG_TGAPLUGIN) << "writeMetadata: there is no enough space for metadata";
        return true;
    }

    // TGA 2.0 developer area
    TgaDeveloperDirectory dir;
    if (d->m_subType == TGAHandlerPrivate::subTypeTGA_V2E()) {
        auto exif = MicroExif::fromImage(image);
        if (!exif.isEmpty()) {
            auto ba = QByteArray("eXif").append(exif.toByteArray(s.byteOrder()));
            TgaDeveloperDirectory::Field f;
            f.tagId = TGA_EXIF_TAGID;
            f.offset = dev->pos();
            f.size = ba.size();
            if (s.writeRawData(ba.data(), ba.size()) != ba.size()) {
                return false;
            }
            dir.fields << f;
        }
        auto icc = image.colorSpace().iccProfile();
        if (!icc.isEmpty()) {
            auto ba = QByteArray("iCCP").append(icc);
            TgaDeveloperDirectory::Field f;
            f.tagId = TGA_ICCP_TAGID;
            f.offset = dev->pos();
            f.size = ba.size();
            if (s.writeRawData(ba.data(), ba.size()) != ba.size()) {
                return false;
            }
            dir.fields << f;
        }
        auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).trimmed();
        if (!xmp.isEmpty()) {
            auto ba = QByteArray("xMPP").append(xmp.toUtf8());
            TgaDeveloperDirectory::Field f;
            f.tagId = TGA_XMPP_TAGID;
            f.offset = dev->pos();
            f.size = ba.size();
            if (s.writeRawData(ba.data(), ba.size()) != ba.size()) {
                return false;
            }
            dir.fields << f;
        }
    }

    // TGA 2.0 extension area
    TgaExtension ext;
    ext.setDateTime(QDateTime::currentDateTimeUtc());
    if (image.hasAlphaChannel()) {
        ext.attributesType = TgaExtension::Alpha;
    }
    auto keys = image.textKeys();
    for (auto &&key : keys) {
        if (!key.compare(QStringLiteral(META_KEY_AUTHOR), Qt::CaseInsensitive)) {
            ext.setAuthor(image.text(key));
            continue;
        }
        if (!key.compare(QStringLiteral(META_KEY_COMMENT), Qt::CaseInsensitive)) {
            ext.setComment(image.text(key));
            continue;
        }
        if (!key.compare(QStringLiteral(META_KEY_DESCRIPTION), Qt::CaseInsensitive)) {
            if (ext.comment().isEmpty())
                ext.setComment(image.text(key));
            continue;
        }
        if (!key.compare(QStringLiteral(META_KEY_SOFTWARE), Qt::CaseInsensitive)) {
            ext.setSoftware(image.text(key));
            continue;
        }
    }

    // write developer area
    if (!dir.isEmpty()) {
        foot.developerOffset = dev->pos();
        s << dir;
    }

    // write extension area (date time is always set)
    foot.extensionOffset = dev->pos();
    s << ext;
    s << foot;

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

bool TGAHandler::readMetadata(QImage &image)
{
    auto dev = device();
    if (dev == nullptr) {
        return false;
    }
    if (dev->isSequential()) {
        qCInfo(LOG_TGAPLUGIN) << "readMetadata: unable to load metadata on a sequential device";
        return true;
    }

    // read TGA footer
    if (!dev->seek(dev->size() - 26)) {
        return false;
    }

    QDataStream s(dev);
    s.setByteOrder(QDataStream::LittleEndian);

    TgaFooter foot;
    s >> foot;
    if (s.status() != QDataStream::Ok) {
        return false;
    }
    if (!foot.isValid()) {
        return true; // not a TGA 2.0 -> no metadata are present
    }

    if (foot.extensionOffset > 0) {
        // read the extension area
        if (!dev->seek(foot.extensionOffset)) {
            return false;
        }

        TgaExtension ext;
        s >> ext;
        if (s.status() != QDataStream::Ok || !ext.isValid()) {
            return false;
        }

        auto dt = ext.dateTime();
        if (dt.isValid()) {
            image.setText(QStringLiteral(META_KEY_MODIFICATIONDATE), dt.toString(Qt::ISODate));
        }
        auto au = ext.author();
        if (!au.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_AUTHOR), au);
        }
        auto cm = ext.comment();
        if (!cm.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_COMMENT), cm);
        }
        auto sw = ext.software();
        if (!sw.isEmpty()) {
            image.setText(QStringLiteral(META_KEY_SOFTWARE), sw);
        }
    }

    if (foot.developerOffset > 0) {
        // read developer area
        if (!dev->seek(foot.developerOffset)) {
            return false;
        }

        TgaDeveloperDirectory dir;
        s >> dir;
        if (s.status() != QDataStream::Ok) {
            return false;
        }

        for (auto &&f : dir.fields) {
            if (!dev->seek(f.offset)) {
                return false;
            }
            if (f.tagId == TGA_EXIF_TAGID) {
                auto ba = dev->read(f.size);
                if (ba.startsWith(QByteArray("eXif"))) {
                    auto exif = MicroExif::fromByteArray(ba.mid(4));
                    exif.updateImageMetadata(image, true);
                    exif.updateImageResolution(image);
                }
                continue;
            }
            if (f.tagId == TGA_ICCP_TAGID) {
                auto ba = dev->read(f.size);
                if (ba.startsWith(QByteArray("iCCP"))) {
                    image.setColorSpace(QColorSpace::fromIccProfile(ba.mid(4)));
                }
                continue;
            }
            if (f.tagId == TGA_XMPP_TAGID) {
                auto ba = dev->read(f.size);
                if (ba.startsWith(QByteArray("xMPP"))) {
                    image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(ba.mid(4)));
                }
                continue;
            }
        }
    }

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

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

void TGAHandler::setOption(ImageOption option, const QVariant &value)
{
    if (option == QImageIOHandler::SubType) {
        auto subType = value.toByteArray();
        auto list = TGAHandler::option(QImageIOHandler::SupportedSubTypes).value<QList<QByteArray>>();
        if (list.contains(subType)) {
            d->m_subType = subType;
        } else {
            d->m_subType = TGAHandlerPrivate::subTypeTGA_V2S();
        }
    }
}

QVariant TGAHandler::option(ImageOption option) const
{
    if (!supportsOption(option)) {
        return {};
    }

    if (option == QImageIOHandler::SupportedSubTypes) {
        return QVariant::fromValue(QList<QByteArray>()
                                   << TGAHandlerPrivate::subTypeTGA_V1() << TGAHandlerPrivate::subTypeTGA_V2S() << TGAHandlerPrivate::subTypeTGA_V2E());
    }

    if (option == QImageIOHandler::SubType) {
        return QVariant::fromValue(d->m_subType);
    }

    auto &&header = d->m_header;
    if (!IsSupported(header)) {
        if (auto dev = device())
            if (!peekHeader(dev, header) && IsSupported(header))
                return {};
        if (!IsSupported(header)) {
            return {};
        }
    }

    if (option == QImageIOHandler::Size) {
        return QVariant::fromValue(QSize(header.width, header.height));
    }

    if (option == QImageIOHandler::ImageFormat) {
        return QVariant::fromValue(imageFormat(header));
    }

    return {};
}

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

    TgaHeader tga;
    if (!peekHeader(device, tga)) {
        qCWarning(LOG_TGAPLUGIN) << "TGAHandler::canRead() error while reading the header";
        return false;
    }

    return IsSupported(tga);
}

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

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

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

#include "moc_tga_p.cpp"
