// vim: set tabstop=4 shiftwidth=4 expandtab:
/*
Gwenview: an image viewer
Copyright 2007 Aurélien Gâteau <agateau@kde.org>

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

*/
// Self
#include "imagemetainfomodel.h"
#include "config-gwenview.h"

// Qt
#include <QLocale>
#include <QSize>

// KF
#include <KFileItem>
#include <KLocalizedString>

// Exiv2
#include <exiv2/exiv2.hpp>

// Local
#include "gwenview_lib_debug.h"
#ifdef HAVE_FITS
#include "imageformats/fitsformat/fitsdata.h"
#include "urlutils.h"
#endif

namespace Gwenview
{
enum GroupRow {
    GeneralGroup,
    ExifGroup,
#ifdef HAVE_FITS
    FitsGroup,
#endif
    IptcGroup,
    XmpGroup,
    NoGroupSpace, // second last entry
    NoGroup, // last entry
};

class MetaInfoGroup
{
public:
    enum {
        InvalidRow = -1,
    };

    class Entry
    {
    public:
        Entry(const QString &key, const QString &label, const QString &value)
            : mKey(key)
            , mLabel(label.trimmed())
            , mValue(value.trimmed())
        {
        }

        QString key() const
        {
            return mKey;
        }
        QString label() const
        {
            return mLabel;
        }

        QString value() const
        {
            return mValue;
        }
        void setValue(const QString &value)
        {
            mValue = value.trimmed();
        }

        void appendValue(const QString &value)
        {
            if (!mValue.isEmpty()) {
                mValue += QLatin1Char('\n');
            }
            mValue += value.trimmed();
        }

    private:
        QString mKey;
        QString mLabel;
        QString mValue;
    };

    MetaInfoGroup(const QString &label)
        : mLabel(label)
    {
    }

    ~MetaInfoGroup()
    {
        qDeleteAll(mList);
    }

    void clear()
    {
        qDeleteAll(mList);
        mList.clear();
        mRowForKey.clear();
    }

    void addEntry(const QString &key, const QString &label, const QString &value)
    {
        addEntry(new Entry(key, label, value));
    }

    void addEntry(Entry *entry)
    {
        mList << entry;
        mRowForKey[entry->key()] = mList.size() - 1;
    }

    void getInfoForKey(const QString &key, QString *label, QString *value) const
    {
        Entry *entry = getEntryForKey(key);
        if (entry) {
            *label = entry->label();
            *value = entry->value();
        }
    }

    QString getKeyAt(int row) const
    {
        Q_ASSERT(row < mList.size());
        return mList[row]->key();
    }

    QString getLabelForKeyAt(int row) const
    {
        Q_ASSERT(row < mList.size());
        return mList[row]->label();
    }

    QString getValueForKeyAt(int row) const
    {
        Q_ASSERT(row < mList.size());
        return mList[row]->value();
    }

    void setValueForKeyAt(int row, const QString &value)
    {
        Q_ASSERT(row < mList.size());
        mList[row]->setValue(value);
    }

    int getRowForKey(const QString &key) const
    {
        return mRowForKey.value(key, InvalidRow);
    }

    int size() const
    {
        return mList.size();
    }

    QString label() const
    {
        return mLabel;
    }

    const QList<Entry *> &entryList() const
    {
        return mList;
    }

private:
    Entry *getEntryForKey(const QString &key) const
    {
        int row = getRowForKey(key);
        if (row == InvalidRow) {
            return nullptr;
        }
        return mList[row];
    }

    QList<Entry *> mList;
    QHash<QString, int> mRowForKey;
    QString mLabel;
};

struct ImageMetaInfoModelPrivate {
    QVector<MetaInfoGroup *> mMetaInfoGroupVector;
    ImageMetaInfoModel *q;

    void clearGroup(MetaInfoGroup *group, const QModelIndex &parent)
    {
        if (group->size() > 0) {
            q->beginRemoveRows(parent, 0, group->size() - 1);
            group->clear();
            q->endRemoveRows();
        }
    }

    void setGroupEntryValue(GroupRow groupRow, const QString &key, const QString &value)
    {
        MetaInfoGroup *group = mMetaInfoGroupVector[groupRow];
        const int entryRow = group->getRowForKey(key);
        if (entryRow == MetaInfoGroup::InvalidRow) {
            qCWarning(GWENVIEW_LIB_LOG) << "No row for key" << key;
            return;
        }
        group->setValueForKeyAt(entryRow, value);
        const QModelIndex groupIndex = q->index(groupRow, 0);
        const QModelIndex entryIndex = q->index(entryRow, 1, groupIndex);
        Q_EMIT q->dataChanged(entryIndex, entryIndex);
    }

    QVariant displayData(const QModelIndex &index) const
    {
        if (index.internalId() == NoGroup) {
            if (index.column() != 0) {
                return {};
            }
            const QString label = mMetaInfoGroupVector[index.row()]->label();
            return QVariant(label);
        }

        if (index.internalId() == NoGroupSpace) {
            return QString();
        }

        MetaInfoGroup *group = mMetaInfoGroupVector[index.internalId()];
        if (index.column() == 0) {
            return group->getLabelForKeyAt(index.row());
        } else {
            return group->getValueForKeyAt(index.row());
        }
    }

    void initGeneralGroup()
    {
        MetaInfoGroup *group = mMetaInfoGroupVector[GeneralGroup];
        group->addEntry(QStringLiteral("General.Name"), i18nc("@item:intable Image file name", "Name"), QString());
        group->addEntry(QStringLiteral("General.Size"), i18nc("@item:intable", "File Size"), QString());
        group->addEntry(QStringLiteral("General.Created"), i18nc("@item:intable", "Date Created"), QString());
        group->addEntry(QStringLiteral("General.Modified"), i18nc("@item:intable", "Date Modified"), QString());
        group->addEntry(QStringLiteral("General.Accessed"), i18nc("@item:intable", "Date Accessed"), QString());
        group->addEntry(QStringLiteral("General.LocalPath"), i18nc("@item:intable", "Path"), QString());
        group->addEntry(QStringLiteral("General.ImageSize"), i18nc("@item:intable", "Image Size"), QString());
        group->addEntry(QStringLiteral("General.Comment"), i18nc("@item:intable", "Comment"), QString());
        group->addEntry(QStringLiteral("General.MimeType"), i18nc("@item:intable", "File Type"), QString());
    }

    template<class Container, class Iterator>
    void fillExivGroup(const QModelIndex &parent, MetaInfoGroup *group, const Container &container, const Exiv2::ExifData &exifData)
    {
        // key aren't always unique (for example, "Iptc.Application2.Keywords"
        // may appear multiple times) so we can't know how many rows we will
        // insert before going through them. That's why we create a hash
        // before.
        using EntryHash = QHash<QString, MetaInfoGroup::Entry *>;
        EntryHash hash;

        Iterator it = container.begin(), end = container.end();

        for (; it != end; ++it) {
            try {
                // Skip metadatum if its tag is an hex number
                if (it->tagName().substr(0, 2) == "0x") {
                    continue;
                }
                const QString key = QString::fromUtf8(it->key().c_str());
                const QString label = QString::fromLocal8Bit(it->tagLabel().c_str());
                std::ostringstream stream;
                it->write(stream, &exifData);
                const QString value = QString::fromLocal8Bit(stream.str().c_str());

                EntryHash::iterator hashIt = hash.find(key);
                if (hashIt != hash.end()) {
                    hashIt.value()->appendValue(value);
                } else {
                    hash.insert(key, new MetaInfoGroup::Entry(key, label, value));
                }
            } catch (const std::out_of_range &error) {
                // Workaround for https://bugs.launchpad.net/ubuntu/+source/exiv2/+bug/1942799
                // which was fixed with https://github.com/Exiv2/exiv2/pull/1918/commits/8a1e949bff482f74599f60b8ab518442036b1834
                qCWarning(GWENVIEW_LIB_LOG) << "Failed to read some meta info:" << error.what();
            } catch (const Exiv2::Error &error) {
                qCWarning(GWENVIEW_LIB_LOG) << "Failed to read some meta info:" << error.what();
            }
        }

        if (hash.isEmpty()) {
            return;
        }
        q->beginInsertRows(parent, 0, hash.size() - 1);
        for (MetaInfoGroup::Entry *entry : qAsConst(hash)) {
            group->addEntry(entry);
        }
        q->endInsertRows();
    }
};

ImageMetaInfoModel::ImageMetaInfoModel()
    : d(new ImageMetaInfoModelPrivate)
{
    d->q = this;
#ifdef HAVE_FITS
    d->mMetaInfoGroupVector.resize(5);
#else
    d->mMetaInfoGroupVector.resize(4);
#endif
    d->mMetaInfoGroupVector[GeneralGroup] = new MetaInfoGroup(i18nc("@title:group General info about the image", "General"));
    d->mMetaInfoGroupVector[ExifGroup] = new MetaInfoGroup(QStringLiteral("EXIF"));
#ifdef HAVE_FITS
    d->mMetaInfoGroupVector[FitsGroup] = new MetaInfoGroup(QStringLiteral("FITS"));
#endif
    d->mMetaInfoGroupVector[IptcGroup] = new MetaInfoGroup(QStringLiteral("IPTC"));
    d->mMetaInfoGroupVector[XmpGroup] = new MetaInfoGroup(QStringLiteral("XMP"));
    d->initGeneralGroup();
}

ImageMetaInfoModel::~ImageMetaInfoModel()
{
    qDeleteAll(d->mMetaInfoGroupVector);
    delete d;
}

static QString formatFileTime(const KFileItem &item, const KFileItem::FileTimes timeType)
{
    return QLocale().toString(item.time(timeType), QLocale::LongFormat);
}

void ImageMetaInfoModel::setDates(const QUrl &url)
{
    KFileItem item(url);
    const QString modifiedString = formatFileTime(item, KFileItem::ModificationTime);
    const QString accessString = formatFileTime(item, KFileItem::AccessTime);
    const QString createdString = formatFileTime(item, KFileItem::CreationTime);

    d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Created"), createdString);
    d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Modified"), modifiedString);
    d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Accessed"), accessString);
}

void ImageMetaInfoModel::setMimeType(const QUrl &url)
{
    KFileItem item(url);

    d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.MimeType"), item.mimetype());
}

void ImageMetaInfoModel::setFileSize(const QUrl &url)
{
    KFileItem item(url);

    d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Size"), KIO::convertSize(item.size()));
}

void ImageMetaInfoModel::setUrl(const QUrl &url)
{
    KFileItem item(url);
    const QString localPathString = item.localPath();

    d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Name"), item.name());
    d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.LocalPath"), localPathString);

#ifdef HAVE_FITS
    if (UrlUtils::urlIsFastLocalFile(url)
        && (url.fileName().endsWith(QLatin1String(".fit"), Qt::CaseInsensitive) || url.fileName().endsWith(QLatin1String(".fits"), Qt::CaseInsensitive))) {
        FITSData fitsLoader;
        MetaInfoGroup *group = d->mMetaInfoGroupVector[FitsGroup];
        QFile file(url.toLocalFile());

        if (!file.open(QIODevice::ReadOnly)) {
            return;
        }

        if (fitsLoader.loadFITS(file)) {
            QString recordList;
            int nkeys = 0;

            fitsLoader.getFITSRecord(recordList, nkeys);
            for (int i = 0; i < nkeys; i++) {
                QString record = recordList.mid(i * 80, 80);
                QString key;
                QString keyStr;
                QString value;

                if (!record.contains(QLatin1Char('='))) {
                    key = record.section(QLatin1Char(' '), 0, 0).simplified();
                    keyStr = key;
                    value = record.section(QLatin1Char(' '), 1, -1).simplified();
                } else {
                    key = record.section(QLatin1Char('='), 0, 0).simplified();
                    if (record.contains(QLatin1Char('/'))) {
                        keyStr = record.section(QLatin1Char('/'), -1, -1).simplified();
                        value = record.section(QLatin1Char('='), 1, -1).section(QLatin1Char('/'), 0, 0);
                    } else {
                        keyStr = key;
                        value = record.section(QLatin1Char('='), 1, -1);
                    }
                    value.remove(QStringLiteral("\'"));
                    value = value.simplified();
                }
                if (value.isEmpty()) {
                    continue;
                }

                // Check if the value is a number and make it more readable
                bool ok = false;
                float number = value.toFloat(&ok);

                if (ok) {
                    value = QString::number(number);
                }

                group->addEntry(QStringLiteral("Fits.") + key, keyStr, value);
            }
        }
    }
#endif
}

void ImageMetaInfoModel::setImageSize(const QSize &size)
{
    QString imageSize;
    if (size.isValid()) {
        imageSize = i18nc("@item:intable %1 is image width, %2 is image height", "%1x%2", size.width(), size.height());

        double megaPixels = size.width() * size.height() / 1000000.;
        if (megaPixels > 0.1) {
            QString megaPixelsString = QString::number(megaPixels, 'f', 1);
            imageSize += QLatin1Char(' ');
            imageSize += i18nc("@item:intable %1 is number of millions of pixels in image", "(%1MP)", megaPixelsString);
        }
    } else {
        imageSize = QLatin1Char('-');
    }
    d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.ImageSize"), imageSize);
}

void ImageMetaInfoModel::setExiv2Image(const Exiv2::Image *image)
{
    MetaInfoGroup *exifGroup = d->mMetaInfoGroupVector[ExifGroup];
    MetaInfoGroup *iptcGroup = d->mMetaInfoGroupVector[IptcGroup];
    MetaInfoGroup *xmpGroup = d->mMetaInfoGroupVector[XmpGroup];
    QModelIndex exifIndex = index(ExifGroup, 0);
    QModelIndex iptcIndex = index(IptcGroup, 0);
    QModelIndex xmpIndex = index(XmpGroup, 0);
    d->clearGroup(exifGroup, exifIndex);
    d->clearGroup(iptcGroup, iptcIndex);
    d->clearGroup(xmpGroup, xmpIndex);

    if (!image) {
        return;
    }

    d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Comment"), QString::fromUtf8(image->comment().c_str()));

    const Exiv2::ExifData &exifData = image->exifData();
    if (image->checkMode(Exiv2::mdExif) & Exiv2::amRead) {
        d->fillExivGroup<Exiv2::ExifData, Exiv2::ExifData::const_iterator>(exifIndex, exifGroup, exifData, exifData);
    }

    if (image->checkMode(Exiv2::mdIptc) & Exiv2::amRead) {
        const Exiv2::IptcData &iptcData = image->iptcData();
        d->fillExivGroup<Exiv2::IptcData, Exiv2::IptcData::const_iterator>(iptcIndex, iptcGroup, iptcData, exifData);
    }

    if (image->checkMode(Exiv2::mdXmp) & Exiv2::amRead) {
        const Exiv2::XmpData &xmpData = image->xmpData();
        d->fillExivGroup<Exiv2::XmpData, Exiv2::XmpData::const_iterator>(xmpIndex, xmpGroup, xmpData, exifData);
    }
}

void ImageMetaInfoModel::getInfoForKey(const QString &key, QString *label, QString *value) const
{
    MetaInfoGroup *group;
    if (key.startsWith(QLatin1String("General"))) {
        group = d->mMetaInfoGroupVector[GeneralGroup];
    } else if (key.startsWith(QLatin1String("Exif"))) {
        group = d->mMetaInfoGroupVector[ExifGroup];
#ifdef HAVE_FITS
    } else if (key.startsWith(QLatin1String("Fits"))) {
        group = d->mMetaInfoGroupVector[FitsGroup];
#endif
    } else if (key.startsWith(QLatin1String("Iptc"))) {
        group = d->mMetaInfoGroupVector[IptcGroup];
    } else if (key.startsWith(QLatin1String("Xmp"))) {
        group = d->mMetaInfoGroupVector[XmpGroup];
    } else {
        qCWarning(GWENVIEW_LIB_LOG) << "Unknown metainfo key" << key;
        return;
    }
    group->getInfoForKey(key, label, value);
}

QString ImageMetaInfoModel::getValueForKey(const QString &key) const
{
    QString label, value;
    getInfoForKey(key, &label, &value);
    return value;
}

QString ImageMetaInfoModel::keyForIndex(const QModelIndex &index) const
{
    if (index.internalId() == NoGroup) {
        return {};
    }
    MetaInfoGroup *group = d->mMetaInfoGroupVector[index.internalId()];
    return group->getKeyAt(index.row());
}

QModelIndex ImageMetaInfoModel::index(int row, int col, const QModelIndex &parent) const
{
    if (col < 0 || col > 1) {
        return {};
    }
    if (!parent.isValid()) {
        // This is a group
        if (row < 0 || row >= d->mMetaInfoGroupVector.size()) {
            return {};
        }
        return createIndex(row, col, col == 0 ? NoGroup : NoGroupSpace);
    } else {
        // This is an entry
        int group = parent.row();
        if (row < 0 || row >= d->mMetaInfoGroupVector[group]->size()) {
            return {};
        }
        return createIndex(row, col, group);
    }
}

QModelIndex ImageMetaInfoModel::parent(const QModelIndex &index) const
{
    if (!index.isValid()) {
        return {};
    }
    if (index.internalId() == NoGroup || index.internalId() == NoGroupSpace) {
        return {};
    } else {
        return createIndex(index.internalId(), 0, NoGroup);
    }
}

int ImageMetaInfoModel::rowCount(const QModelIndex &parent) const
{
    if (!parent.isValid()) {
        return d->mMetaInfoGroupVector.size();
    } else if (parent.internalId() == NoGroup) {
        return d->mMetaInfoGroupVector[parent.row()]->size();
    } else {
        return 0;
    }
}

int ImageMetaInfoModel::columnCount(const QModelIndex & /*parent*/) const
{
    return 2;
}

QVariant ImageMetaInfoModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid()) {
        return {};
    }

    switch (role) {
    case Qt::DisplayRole:
        return d->displayData(index);
    default:
        return {};
    }
}

QVariant ImageMetaInfoModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (orientation == Qt::Vertical || role != Qt::DisplayRole) {
        return {};
    }

    QString caption;
    if (section == 0) {
        caption = i18nc("@title:column", "Property");
    } else if (section == 1) {
        caption = i18nc("@title:column", "Value");
    } else {
        qCWarning(GWENVIEW_LIB_LOG) << "Unknown section" << section;
    }

    return QVariant(caption);
}

} // namespace

#include "moc_imagemetainfomodel.cpp"
