/*
 *  SPDX-FileCopyrightText: 2008-2010 Cyrille Berger <cberger@cberger.net>
 *  SPDX-FileCopyrightText: 2021 L. E. Segovia <amy@amyspark.me>
 *
 *  SPDX-License-Identifier: LGPL-2.0-or-later
 */
#include "kis_xmp_io.h"

#include <string>

#include <QIODevice>
#include <QRegularExpression>

#include <kis_exiv2_common.h>
#include <kis_meta_data_entry.h>
#include <kis_meta_data_parser.h>
#include <kis_meta_data_schema.h>
#include <kis_meta_data_schema_registry.h>
#include <kis_meta_data_store.h>
#include <kis_meta_data_type_info.h>
#include <kis_meta_data_value.h>
#include <KisUsageLogger.h>


#include <kis_debug.h>

KisXMPIO::KisXMPIO()
    : KisMetaData::IOBackend()
{
}

KisXMPIO::~KisXMPIO()
{
}

inline std::string exiv2Prefix(const KisMetaData::Schema *_schema)
{
    const QByteArray latin1SchemaUri = _schema->uri().toLatin1();
    std::string prefix = Exiv2::XmpProperties::prefix(latin1SchemaUri.constData());
    if (prefix.empty()) {
        dbgMetaData << "Unknown namespace " << ppVar(_schema->uri()) << ppVar(_schema->prefix());
        prefix = _schema->prefix().toLatin1().constData();
        Exiv2::XmpProperties::registerNs(latin1SchemaUri.constData(), prefix);
    }
    return prefix;
}

namespace
{
void saveStructure(Exiv2::XmpData &xmpData_,
                   const QString &name,
                   const std::string &prefix,
                   const QMap<QString, KisMetaData::Value> &structure,
                   const KisMetaData::Schema *structureSchema)
{
    std::string structPrefix = exiv2Prefix(structureSchema);
    for (QMap<QString, KisMetaData::Value>::const_iterator it = structure.begin(); it != structure.end(); ++it) {
        Q_ASSERT(it.value().type() != KisMetaData::Value::Structure); // Can't nest structure
        QString key = QString("%1/%2:%3").arg(name).arg(structPrefix.c_str()).arg(it.key());
        Exiv2::XmpKey ekey(prefix, key.toLatin1().constData());
        dbgMetaData << ppVar(key) << ppVar(ekey.key().c_str());
        Exiv2::Value *v = kmdValueToExivXmpValue(it.value());
        if (v) {
            xmpData_.add(ekey, v);
        }
    }
}
}

bool KisXMPIO::saveTo(const KisMetaData::Store *store, QIODevice *ioDevice, HeaderType headerType) const
{
    dbgMetaData << "Save XMP Data";
    Exiv2::XmpData xmpData_;

    for (const KisMetaData::Entry &entry : *store) {
        // Check whether the prefix and namespace are know to exiv2
        std::string prefix = exiv2Prefix(entry.schema());
        dbgMetaData << "Saving " << entry.name();

        const KisMetaData::Value &value = entry.value();

        const KisMetaData::TypeInfo *typeInfo = entry.schema()->propertyType(entry.name());
        if (value.type() == KisMetaData::Value::Structure) {
            QMap<QString, KisMetaData::Value> structure = value.asStructure();
            const KisMetaData::Schema *structureSchema = 0;
            if (typeInfo) {
                structureSchema = typeInfo->structureSchema();
            }
            if (!structureSchema) {
                dbgMetaData << "Unknown schema for " << entry.name();
                structureSchema = entry.schema();
            }
            Q_ASSERT(structureSchema);
            saveStructure(xmpData_, entry.name(), prefix, structure, structureSchema);
        } else {
            Exiv2::XmpKey key(prefix, entry.name().toLatin1().constData());
            if (typeInfo
                && (typeInfo->propertyType() == KisMetaData::TypeInfo::OrderedArrayType
                    || typeInfo->propertyType() == KisMetaData::TypeInfo::UnorderedArrayType
                    || typeInfo->propertyType() == KisMetaData::TypeInfo::AlternativeArrayType)
                && typeInfo->embeddedPropertyType()->propertyType() == KisMetaData::TypeInfo::StructureType) {
                // Here is the bad part, again we need to do it by hand
                Exiv2::XmpTextValue tv;
                switch (typeInfo->propertyType()) {
                case KisMetaData::TypeInfo::OrderedArrayType:
                    tv.setXmpArrayType(Exiv2::XmpValue::xaSeq);
                    break;
                case KisMetaData::TypeInfo::UnorderedArrayType:
                    tv.setXmpArrayType(Exiv2::XmpValue::xaBag);
                    break;
                case KisMetaData::TypeInfo::AlternativeArrayType:
                    tv.setXmpArrayType(Exiv2::XmpValue::xaAlt);
                    break;
                default:
                    // Cannot happen
                    ;
                }
                xmpData_.add(key, &tv); // set the array type
                const KisMetaData::TypeInfo *structureTypeInfo = typeInfo->embeddedPropertyType();
                const KisMetaData::Schema *structureSchema = 0;
                if (structureTypeInfo) {
                    structureSchema = structureTypeInfo->structureSchema();
                }
                if (!structureSchema) {
                    dbgMetaData << "Unknown schema for " << entry.name();
                    structureSchema = entry.schema();
                }
                Q_ASSERT(structureSchema);
                QList<KisMetaData::Value> array = value.asArray();
                for (int idx = 0; idx < array.size(); ++idx) {
                    saveStructure(xmpData_,
                                  QString("%1[%2]").arg(entry.name()).arg(idx + 1),
                                  prefix,
                                  array[idx].asStructure(),
                                  structureSchema);
                }
            } else {
                dbgMetaData << ppVar(key.key().c_str());
                Exiv2::Value *v = kmdValueToExivXmpValue(value);
                if (v && v->typeId() != Exiv2::TypeId::invalidTypeId) {
                    xmpData_.add(key, v);
                } else {
                    warnMetaData << "Invalid metadata value " << value << "for: " << entry.name();
                    KisUsageLogger::log(QString("Invalid metadata value \"%1\" for key %2. The metadata key is therefore skipped during saving.").arg(value.toString(), entry.name()));
                }
            }
        }
        // TODO property qualifier
    }
    // Serialize data
    std::string xmpPacket_;
    try {
        Exiv2::XmpParser::encode(xmpPacket_, xmpData_);
    } catch (std::exception &e) {
        warnMetaData << "Couldn't encode the data, error =" << e.what();
        return false;
    }
    // Save data into the IO device
    ioDevice->open(QIODevice::WriteOnly);
    if (headerType == KisMetaData::IOBackend::JpegHeader) {
        xmpPacket_ = "http://ns.adobe.com/xap/1.0/\0" + xmpPacket_;
    }
    ioDevice->write(xmpPacket_.c_str(), xmpPacket_.length());
    return true;
}

bool parseTagName(const QString &tagString,
                  QString &structName,
                  int &arrayIndex,
                  QString &tagName,
                  const KisMetaData::TypeInfo **typeInfo,
                  const KisMetaData::Schema *schema)
{
    arrayIndex = -1;
    *typeInfo = 0;

    int numSubNames = tagString.count('/') + 1;

    if (numSubNames == 1) {
        structName.clear();
        tagName = tagString;
        *typeInfo = schema->propertyType(tagName);
        return true;
    }

    if (numSubNames == 2) {
        /**
         * We use Unicode-matching mode, because according to XML specification
         * the name of an entity can contain Unicode characters.
         */
        QRegularExpression regexp("([A-Za-z]\\w+)/([A-Za-z]\\w+):([A-Za-z]\\w+)");
        regexp.setPatternOptions(QRegularExpression::UseUnicodePropertiesOption);

        QRegularExpressionMatch match;
        if (tagString.contains(regexp, &match)) {
            structName = match.captured(1);
            tagName = match.captured(3);
            *typeInfo = schema->propertyType(structName);

            if (*typeInfo && (*typeInfo)->propertyType() == KisMetaData::TypeInfo::StructureType) {
                *typeInfo = (*typeInfo)->structureSchema()->propertyType(tagName);
            }

            return true;
        }

        QRegularExpression regexp2("([A-Za-z]\\w+)\\[(\\d+)\\]/([A-Za-z]\\w+):([A-Za-z]\\w+)");
        regexp2.setPatternOptions(QRegularExpression::UseUnicodePropertiesOption);

        QRegularExpressionMatch match2;
        if (tagString.contains(regexp2, &match2)) {
            structName = match2.captured(1);
            arrayIndex = match2.captured(2).toInt() - 1;
            tagName = match2.captured(4);

            if (schema->propertyType(structName)) {
                *typeInfo = schema->propertyType(structName)->embeddedPropertyType();
                Q_ASSERT(*typeInfo);

                if ((*typeInfo)->propertyType() == KisMetaData::TypeInfo::StructureType) {
                    *typeInfo = (*typeInfo)->structureSchema()->propertyType(tagName);
                }
            }

            return true;
        }
    }

    warnKrita << "WARNING: Unsupported tag. We do not yet support nested tags. The tag will be dropped!";
    warnKrita << "         Failing tag:" << tagString;
    return false;
}

bool KisXMPIO::loadFrom(KisMetaData::Store *store, QIODevice *ioDevice) const
{
    ioDevice->open(QIODevice::ReadOnly);
    dbgMetaData << "Load XMP Data";
    std::string xmpPacket_;
    QByteArray arr = ioDevice->readAll();
    xmpPacket_.assign(arr.data(), arr.length());
    dbgMetaData << xmpPacket_.length();
    //     dbgMetaData << xmpPacket_.c_str();
    Exiv2::XmpData xmpData_;
    if (Exiv2::XmpParser::decode(xmpData_, xmpPacket_) != 0) {
        warnMetaData << "Failed to decode as XMP";
        return false;
    }
    QMap<const KisMetaData::Schema *, QMap<QString, QMap<QString, KisMetaData::Value>>> structures;
    QMap<const KisMetaData::Schema *, QMap<QString, QVector<QMap<QString, KisMetaData::Value>>>> arraysOfStructures;
    for (Exiv2::XmpData::iterator it = xmpData_.begin(); it != xmpData_.end(); ++it) {
        dbgMetaData << "Start iteration" << it->key().c_str();

        Exiv2::XmpKey key(it->key());
        dbgMetaData << key.groupName().c_str() << " " << key.tagName().c_str() << " " << key.ns().c_str();
        if ((key.groupName() == "exif" || key.groupName() == "tiff")
            && key.tagName() == "NativeDigest") { // TODO: someone who has time to lose can look in adding support for
                                                  // NativeDigest, it's undocumented use by the XMP SDK to check if exif
                                                  // data has been changed while XMP hasn't been updated
            dbgMetaData << "dropped";
        } else {
            const KisMetaData::Schema *schema =
                KisMetaData::SchemaRegistry::instance()->schemaFromPrefix(key.groupName().c_str());
            if (!schema) {
                schema = KisMetaData::SchemaRegistry::instance()->schemaFromUri(key.ns().c_str());
                if (!schema) {
                    schema = KisMetaData::SchemaRegistry::instance()->create(key.ns().c_str(), key.groupName().c_str());
                    Q_ASSERT(schema);
                }
            }
#if EXIV2_TEST_VERSION(0,28,0)
            const Exiv2::Value::UniquePtr value = it->getValue();
#else
            const Exiv2::Value::AutoPtr value = it->getValue();
#endif
            QString structName;
            int arrayIndex = -1;
            QString tagName;
            const KisMetaData::TypeInfo *typeInfo = 0;

            if (!parseTagName(key.tagName().c_str(), structName, arrayIndex, tagName, &typeInfo, schema))
                continue;

            bool isStructureEntry = !structName.isEmpty() && arrayIndex == -1;
            bool isStructureInArrayEntry = !structName.isEmpty() && arrayIndex != -1;
            Q_ASSERT(isStructureEntry != isStructureInArrayEntry || !isStructureEntry);

            KisMetaData::Value v;
            bool ignoreValue = false;
            // Compute the value
            if (value->typeId() == Exiv2::xmpBag || value->typeId() == Exiv2::xmpSeq
                || value->typeId() == Exiv2::xmpAlt) {
                const KisMetaData::TypeInfo *embeddedTypeInfo = 0;
                if (typeInfo) {
                    embeddedTypeInfo = typeInfo->embeddedPropertyType();
                }
                const KisMetaData::Parser *parser = 0;
                if (embeddedTypeInfo) {
                    parser = embeddedTypeInfo->parser();
                }
                const Exiv2::XmpArrayValue *xav = dynamic_cast<const Exiv2::XmpArrayValue *>(value.get());
                Q_ASSERT(xav);
                QList<KisMetaData::Value> array;
#if EXIV2_TEST_VERSION(0,28,0)
                for (size_t i = 0; i < xav->count(); ++i) {
#else
                for (int i = 0; i < xav->count(); ++i) {
#endif
                    QString value = QString::fromStdString(xav->toString(i));
                    if (parser) {
                        KisMetaData::Value parsed = parser->parse(value);
                        if (parsed.type() == KisMetaData::Value::Invalid) {
                            KisUsageLogger::log(QString("Cannot parse metadata value \"%1\" for %2. Assigning invalid value.").arg(value, tagName));
                        }
                        array.push_back(parsed);

                    } else {
                        dbgImage << "No parser " << tagName;
                        array.push_back(KisMetaData::Value(value));
                    }
                }
                KisMetaData::Value::ValueType vt = KisMetaData::Value::Invalid;
                switch (xav->xmpArrayType()) {
                case Exiv2::XmpValue::xaNone:
                    warnKrita << "KisXMPIO: Unsupported array";
                    break;
                case Exiv2::XmpValue::xaAlt:
                    vt = KisMetaData::Value::AlternativeArray;
                    break;
                case Exiv2::XmpValue::xaBag:
                    vt = KisMetaData::Value::UnorderedArray;
                    break;
                case Exiv2::XmpValue::xaSeq:
                    vt = KisMetaData::Value::OrderedArray;
                    break;
                }
                v = KisMetaData::Value(array, vt);
            } else if (value->typeId() == Exiv2::langAlt) {
                const Exiv2::LangAltValue *xav = dynamic_cast<const Exiv2::LangAltValue *>(value.get());
                KIS_ASSERT(xav);

                QList<KisMetaData::Value> alt;
                for (std::map<std::string, std::string>::const_iterator it = xav->value_.begin();
                     it != xav->value_.end();
                     ++it) {
                    KisMetaData::Value valt(it->second.c_str());
                    valt.addPropertyQualifier("xml:lang", KisMetaData::Value(it->first.c_str()));
                    alt.push_back(valt);
                }
                v = KisMetaData::Value(alt, KisMetaData::Value::LangArray);
            } else {
                QString valTxt = value->toString().c_str();
                if (typeInfo && typeInfo->parser()) {
                    v = typeInfo->parser()->parse(valTxt);
                    if (v.type() == KisMetaData::Value::Invalid) {
                        KisUsageLogger::log(QString("Cannot parse metadata value \"%1\" for %2. Assigning invalid value.").arg(valTxt, tagName));
                    }
                } else {
                    dbgMetaData << "No parser " << tagName;
                    v = KisMetaData::Value(valTxt);
                }
                if (valTxt == "type=\"Struct\"") {
                    if (!typeInfo || typeInfo->propertyType() == KisMetaData::TypeInfo::StructureType) {
                        ignoreValue = true;
                    }
                }
            }

            // set the value
            if (isStructureEntry) {
                structures[schema][structName][tagName] = v;
            } else if (isStructureInArrayEntry) {
                if (arraysOfStructures[schema][structName].size() <= arrayIndex) {
                    arraysOfStructures[schema][structName].resize(arrayIndex + 1);
                }

                if (!arraysOfStructures[schema][structName][arrayIndex].contains(tagName)) {
                    arraysOfStructures[schema][structName][arrayIndex][tagName] = v;
                } else {
                    warnKrita << "WARNING: trying to overwrite tag" << tagName << "in" << structName << arrayIndex;
                }
            } else {
                if (!ignoreValue) {
                    store->addEntry(KisMetaData::Entry(schema, tagName, v));
                } else {
                    dbgMetaData << "Ignoring value for " << tagName << " " << v;
                }
            }
        }
    }

    for (QMap<const KisMetaData::Schema *, QMap<QString, QMap<QString, KisMetaData::Value>>>::iterator it =
             structures.begin();
         it != structures.end();
         ++it) {
        const KisMetaData::Schema *schema = it.key();
        for (QMap<QString, QMap<QString, KisMetaData::Value>>::iterator it2 = it.value().begin();
             it2 != it.value().end();
             ++it2) {
            store->addEntry(KisMetaData::Entry(schema, it2.key(), KisMetaData::Value(it2.value())));
        }
    }
    for (QMap<const KisMetaData::Schema *, QMap<QString, QVector<QMap<QString, KisMetaData::Value>>>>::iterator it =
             arraysOfStructures.begin();
         it != arraysOfStructures.end();
         ++it) {
        const KisMetaData::Schema *schema = it.key();
        for (QMap<QString, QVector<QMap<QString, KisMetaData::Value>>>::iterator it2 = it.value().begin();
             it2 != it.value().end();
             ++it2) {
            KisMetaData::Value::ValueType type = KisMetaData::Value::OrderedArray;
            QString entryName = it2.key();
            if (schema->propertyType(entryName)) {
                switch (schema->propertyType(entryName)->propertyType()) {
                case KisMetaData::TypeInfo::OrderedArrayType:
                    type = KisMetaData::Value::OrderedArray;
                    break;
                case KisMetaData::TypeInfo::UnorderedArrayType:
                    type = KisMetaData::Value::OrderedArray;
                    break;
                case KisMetaData::TypeInfo::AlternativeArrayType:
                    type = KisMetaData::Value::AlternativeArray;
                    break;
                default:
                    type = KisMetaData::Value::Invalid;
                    break;
                }
            } else if (store->containsEntry(schema, entryName)) {
                KisMetaData::Value value = store->getEntry(schema, entryName).value();
                if (value.isArray()) {
                    type = value.type();
                }
            }
            store->removeEntry(schema, entryName);
            if (type != KisMetaData::Value::Invalid) {
                QList<KisMetaData::Value> valueList;
                for (int i = 0; i < it2.value().size(); ++i) {
                    valueList.append(it2.value()[i]);
                }
                store->addEntry(KisMetaData::Entry(schema, entryName, KisMetaData::Value(valueList, type)));
            }
        }
    }

    return true;
}
