/*
    This file is part of Kiten, a KDE Japanese Reference Tool
    SPDX-FileCopyrightText: 2001 Jason Katz-Brown <jason@katzbrown.com>
    SPDX-FileCopyrightText: 2006 Joseph Kerian <jkerian@gmail.com>
    SPDX-FileCopyrightText: 2006 Eric Kjeldergaard <kjelderg@gmail.com>
    SPDX-FileCopyrightText: 2011 Daniel E. Moctezuma <democtezuma@gmail.com>

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

#include "entry.h"

#include <KLocalizedString>

using namespace Qt::StringLiterals;

/**
 * The default constructor, unless you really know what you're doing,
 * THIS SHOULD NOT BE USED. For general use, other entities will need
 * to have the information provided by the other constructors
 * (particularly the sourceDictionary).
 */
Entry::Entry()
{
    init();
}

Entry::Entry(const QString &sourceDictionary)
    : sourceDict(sourceDictionary)
{
    init();
}

Entry::Entry(const QString &sourceDictionary, const QString &word, const QStringList &reading, const QStringList &meanings)
    : Word(word)
    , Meanings(meanings)
    , Readings(reading)
    , sourceDict(sourceDictionary)
{
    init();
}

Entry::Entry(const Entry &src)
    : Word(src.Word)
    , Meanings(src.Meanings)
    , Readings(src.Readings)
    , ExtendedInfo(src.ExtendedInfo)
    , sourceDict(src.sourceDict)
{
    outputListDelimiter = src.outputListDelimiter;
}

bool Entry::extendedItemCheck(const QString &key, const QString &value) const
{
    return getExtendedInfoItem(key) == value;
}

/**
 * Get the dictionary name that generated this Entry. I can't think of a reason to be changing this
 */
QString Entry::getDictName() const
{
    return sourceDict;
}

/**
 * Get the word from this Entry. If the entry is of type kanji/kana/meaning/etc, this will return
 * the kanji. If it is of kana/meaning/etc, it will return kana.
 */
QString Entry::getWord() const
{
    return Word;
}

/**
 * Get a QString containing all of the meanings known, connected by the outputListDelimiter
 */
QString Entry::getMeanings() const
{
    return Meanings.join(outputListDelimiter);
}

/**
 * Simple accessor
 */
QStringList Entry::getMeaningsList() const
{
    return Meanings;
}

/**
 * Simple accessor
 */
QString Entry::getReadings() const
{
    return Readings.join(outputListDelimiter);
}

/**
 * Simple accessor
 */
QStringList Entry::getReadingsList() const
{
    return Readings;
}

/**
 * Simple accessor
 */
QHash<QString, QString> Entry::getExtendedInfo() const
{
    return ExtendedInfo;
}

/**
 * Simple accessor
 *
 * @param x the key for the extended info item to get
 */
QString Entry::getExtendedInfoItem(const QString &x) const
{
    return ExtendedInfo[x];
}

/**
 * Prepares Meanings for output as HTML
 */
inline QString Entry::HTMLMeanings() const
{
    return QStringLiteral("<span class=\"Meanings\">%1</span>").arg(Meanings.join(outputListDelimiter));
}

/* Prepares Readings for output as HTML */
inline QString Entry::HTMLReadings() const
{
    QStringList list;
    for (const QString &it : Readings) {
        list += makeLink(it);
    }

    return QStringLiteral("<span class=\"Readings\">%1</span>").arg(list.join(outputListDelimiter));
}

/**
 * Prepares Word for output as HTML
 */
inline QString Entry::HTMLWord() const
{
    return QStringLiteral("<span class=\"Word\">%1</span>").arg(Word);
}

void Entry::init()
{
    outputListDelimiter = i18n("; ");
}

/**
 * Determines whether @param character is a kanji character.
 */
bool Entry::isKanji(const QChar &character) const
{
    ushort value = character.unicode();
    if (value < 255) {
        return false;
    }
    if (0x3040 <= value && value <= 0x30FF) {
        return false; // Kana
    }

    return true; // Note our folly here... we assuming any non-ascii/kana is kanji
}

/**
 * Returns true if all members of test are in list
 */
bool Entry::listMatch(const QStringList &list, const QStringList &test, DictQuery::MatchType type) const
{
    if (type == DictQuery::Exact) {
        for (const QString &it : test) {
            if (!list.contains(it)) {
                return false;
            }
        }
    } else if (type == DictQuery::Beginning) {
        for (const QString &it : test) {
            bool found = false;
            for (const QString &it2 : list) {
                if (it2.startsWith(it)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                return false;
            }
        }
    } else if (type == DictQuery::Ending) {
        for (const QString &it : test) {
            bool found = false;
            for (const QString &it2 : list) {
                if (it2.endsWith(it)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                return false;
            }
        }
    } else {
        for (const QString &it : test) {
            bool found = false;
            for (const QString &it2 : list) {
                if (it2.contains(it)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                return false;
            }
        }
    }

    return true;
}

/**
 * New functions for Entry doing direct display.
 *
 * Creates a link for the given @p entryString.
 */
inline QString Entry::makeLink(const QString &entryString) const
{
    return QStringLiteral("<a href=\"%1\">%1</a>").arg(entryString);
}

bool Entry::matchesQuery(const DictQuery &query) const
{
    if (!query.getWord().isEmpty()) {
        if (query.getMatchType() == DictQuery::Exact && this->getWord() != query.getWord()) {
            return false;
        }
        if (query.getMatchType() == DictQuery::Beginning && !this->getWord().startsWith(query.getWord())) {
            return false;
        }
        if (query.getMatchType() == DictQuery::Ending && !this->getWord().endsWith(query.getWord())) {
            return false;
        }
        if (query.getMatchType() == DictQuery::Anywhere && !this->getWord().contains(query.getWord())) {
            return false;
        }
    }

    if (!query.getPronunciation().isEmpty() && !getReadings().isEmpty()) {
        if (!listMatch(Readings, query.getPronunciation().split(DictQuery::mainDelimiter), query.getMatchType())) {
            return false;
        }
    }

    if (!query.getPronunciation().isEmpty() && getReadings().isEmpty() && !getWord().isEmpty()) {
        switch (query.getMatchType()) {
        case DictQuery::Exact:
            if (getWord() != query.getPronunciation()) {
                return false;
            }
            break;
        case DictQuery::Beginning:
            if (!getWord().startsWith(query.getPronunciation())) {
                return false;
            }
            break;
        case DictQuery::Ending:
            if (!getWord().endsWith(query.getPronunciation())) {
                return false;
            } // fallthrough
        case DictQuery::Anywhere:
            if (!getWord().contains(query.getPronunciation())) {
                return false;
            }
            break;
        }
    }

    if (!query.getMeaning().isEmpty()) {
        if (!listMatch(Meanings.join(QLatin1Char(' ')).toLower().split(' '_L1),
                       query.getMeaning().toLower().split(DictQuery::mainDelimiter),
                       query.getMatchType())) {
            return false;
        }
    }

    QList<QString> propList = query.listPropertyKeys();
    for (const QString &key : propList) {
        if (!extendedItemCheck(key, query.getProperty(key))) {
            return false;
        }
    }

    return true;
}

/**
 * Main switching function for displaying to the user
 */
QString Entry::toHTML() const
{
    return QStringLiteral("<div class=\"Entry\">%1%2%3</div>").arg(HTMLWord()).arg(HTMLReadings()).arg(HTMLMeanings());
}

inline QString Entry::toKVTML() const
{
    /*
<e m="1" s="1">
       <o width="414" l="en" q="t">(eh,) excuse me</o>
       <t width="417" l="jp" q="o">(あのう、) すみません </t>
</e>
*/
    // TODO: en should not necessarily be the language here.
    return QStringLiteral(
               "<e>\n<o l=\"en\">%1</o>\n"
               "<t l=\"jp-kanji\">%2</t>\n"
               "<t l=\"jp-kana\">%3</t></e>\n\n")
        .arg(getMeanings())
        .arg(getWord())
        .arg(getReadings());
}

/**
 * This method should return the entry object in a simple QString format
 * Brief form should be usable in quick summaries, for example
 * Verbose form might be used to save a complete list of entries to a file, for example.
 */
QString Entry::toString() const
{
    return QStringLiteral("%1 (%2) %3").arg(Word).arg(getReadings()).arg(getMeanings());
}

/**
 * This version of sort only sorts dictionaries...
 * This is a replacement for an operator\< function... so we return true if
 * "this" should show up first on the list.
 */
bool Entry::sort(const Entry &that, const QStringList &dictOrder, const QStringList &fields) const
{
    if (this->sourceDict != that.sourceDict) {
        for (const QString &dict : dictOrder) {
            if (dict == that.sourceDict) {
                return false;
            }
            if (dict == this->sourceDict) {
                return true;
            }
        }
    } else {
        for (const QString &field : fields) {
            if (field == QLatin1String("Word/Kanji")) {
                return this->getWord() < that.getWord();
            } else if (field == QLatin1String("Meaning")) {
                return listMatch(that.getMeaningsList(), this->getMeaningsList(), DictQuery::Exact)
                    && (that.getMeaningsList().count() != this->getMeaningsList().count());
            } else if (field == QLatin1String("Reading")) {
                return listMatch(that.getReadingsList(), this->getReadingsList(), DictQuery::Exact)
                    && (that.getReadingsList().count() != this->getReadingsList().count());
            } else {
                const QString thisOne = this->getExtendedInfoItem(field);
                const QString thatOne = that.getExtendedInfoItem(field);
                // Only sort by this field if the values differ, otherwise move to the next field
                if (thisOne != thatOne) {
                    // If the second item does not have this field, sort this one first
                    if (thatOne.isEmpty()) {
                        return true;
                    }
                    // If we don't have this field, sort "this" to second
                    if (thisOne.isEmpty()) {
                        return false;
                    }
                    // Otherwise, send it to a virtual function (to allow dictionaries to override sorting)
                    return this->sortByField(that, field);
                }
            }
        }
    }
    return false; // If we reach here, they match as much as possible
}

bool Entry::sortByField(const Entry &that, const QString &field) const
{
    return this->getExtendedInfoItem(field) < that.getExtendedInfoItem(field);
}
