/*
 * Fooyin
 * Copyright © 2023, Luke Taylor <luket@pm.me>
 *
 * Fooyin 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 3 of the License, or
 * (at your option) any later version.
 *
 * Fooyin 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 Fooyin.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "tageditormodel.h"

#include "tageditoritem.h"
#include "tageditorpopulator.h"
#include "tageditorsettings.h"

#include <core/constants.h>
#include <core/library/libraryutils.h>
#include <core/scripting/scripttrackwriter.h>
#include <gui/guisettings.h>
#include <utils/helpers.h>
#include <utils/settings/settingsmanager.h>
#include <utils/stardelegate.h>
#include <utils/starrating.h>
#include <utils/stringutils.h>

using namespace Qt::StringLiterals;

constexpr auto MultipleValuesPrefix = "<<multiple values>>";

namespace Fooyin::TagEditor {
namespace {
QStringList splitEditorValues(const QString& value, const QStringList& separators)
{
    return splitMultiValueText(value, separators);
}

QString joinEditorValues(const QStringList& values, const QStringList& separators)
{
    return joinMultiValueText(values, separators);
}

QStringList capitaliseValues(QStringList values)
{
    std::ranges::transform(values, values.begin(), Utils::capitalise);
    return values;
}

bool hasDistinctValues(const QStringList& values)
{
    if(values.size() < 2) {
        return false;
    }

    const QString& firstValue = values.front();
    return std::ranges::any_of(values.cbegin() + 1, values.cend(),
                               [&firstValue](const QString& value) { return value != firstValue; });
}

QStringList capitalisedTrackValues(const TrackList& tracks, const TagEditorField& field, const QStringList& separators)
{
    QStringList values;
    values.reserve(static_cast<qsizetype>(tracks.size()));

    for(const auto& track : tracks) {
        const QString trackValue = track.metaValue(field.scriptField);

        if(field.multivalue) {
            values.append(joinEditorValues(
                capitaliseValues(trackValue.split(QLatin1String{Constants::UnitSeparator}, Qt::KeepEmptyParts)),
                separators));
        }
        else {
            values.append(Utils::capitalise(trackValue));
        }
    }

    return values;
}

bool updateItemTrackValues(TagEditorItem& item, const QStringList& values, const QStringList& separators)
{
    if(values.empty()) {
        return false;
    }

    if(hasDistinctValues(values)) {
        if(item.setValue(joinEditorValues(values, separators), separators)) {
            item.setMultipleValues(true);
            item.setSplitTrackValues(true);
            return true;
        }
        return false;
    }

    if(item.setValue(values.front(), separators)) {
        item.setMultipleValues(false);
        item.setSplitTrackValues(false);
        return true;
    }

    return false;
}

bool shouldTreatAsMultiValue(const TagEditorField& field)
{
    return (Track::isMultiValueTag(field.scriptField) || Track::isExtraTag(field.scriptField)) && field.multivalue;
}
} // namespace

class TagEditorModelPrivate
{
public:
    explicit TagEditorModelPrivate(TagEditorModel* self, SettingsManager* settings)
        : m_self{self}
        , m_settings{settings}
    { }

    bool hasTagConflict(TagEditorItem* item, const QString& title) const;
    bool isDefaultField(const QString& name) const;

    void reset();

    bool updateTrackMetadata(const TagEditorField& field, const QVariant& value, bool split = false);
    [[nodiscard]] QStringList multiValueSeparators() const;

    TagEditorModel* m_self;

    SettingsManager* m_settings;

    TrackList m_tracks;

    QString m_defaultFieldtext{u"<input field name>"_s};
    std::vector<TagEditorField> m_fields;
    int m_ratingRow{-1};

    TagEditorItem m_root;
    TagEditorTagMap m_tags;
};

bool TagEditorModelPrivate::hasTagConflict(TagEditorItem* item, const QString& title) const
{
    for(const auto& [key, tag] : m_tags) {
        if(item != &tag && (title == key || tag.title() == title || tag.changedTitle() == title)) {
            return true;
        }
    }
    return false;
}

bool TagEditorModelPrivate::isDefaultField(const QString& name) const
{
    return std::ranges::any_of(std::as_const(m_fields), [name](const auto& field) { return field.name == name; });
}

QStringList TagEditorModelPrivate::multiValueSeparators() const
{
    return TagEditor::multiValueSeparators(*m_settings);
}

void TagEditorModelPrivate::reset()
{
    m_root = {};
    m_tags.clear();
}

bool TagEditorModelPrivate::updateTrackMetadata(const TagEditorField& field, const QVariant& value, bool split)
{
    if(m_tracks.empty() || field.scriptField.isEmpty()) {
        return false;
    }

    QString tag{field.scriptField};

    const auto checkTag = [&tag](const char* metadataField) {
        return tag.compare(QLatin1String{metadataField}, Qt::CaseInsensitive) == 0;
    };

    const bool isList   = split || field.multivalue;
    const bool isRating = checkTag(Constants::MetaData::Rating) || checkTag(Constants::MetaData::RatingEditor)
                       || checkTag(Constants::MetaData::RatingNormalized) || checkTag(Constants::MetaData::Stars);

    QStringList listValue;
    float floatValue{-1};

    if(isList) {
        listValue = splitMultiValueText(value.toString(), multiValueSeparators());
    }
    else if(isRating) {
        bool validFloat{false};
        floatValue = value.toFloat(&validFloat);
        if(!validFloat) {
            floatValue = -1;
        }
    }

    for(int i{0}; Track& track : m_tracks) {
        if(track.hasCue() && !isRating) {
            continue;
        }

        if(split && std::cmp_less(i, listValue.size())) {
            if(isRating) {
                setTrackScriptValue(tag, listValue.at(i).toFloat(), track);
            }
            else {
                setTrackScriptValue(tag, listValue.at(i), track);
            }
        }
        else if(isList) {
            setTrackScriptValue(tag, listValue, track);
        }
        else if(isRating) {
            setTrackScriptValue(tag, floatValue, track);
        }
        else {
            setTrackScriptValue(tag, value.toString(), track);
        }

        ++i;
    }

    return true;
}

TagEditorModel::TagEditorModel(SettingsManager* settings, QObject* parent)
    : ExtendableTableModel{parent}
    , p{std::make_unique<TagEditorModelPrivate>(this, settings)}
{ }

TagEditorModel::~TagEditorModel() = default;

TrackList TagEditorModel::tracks() const
{
    return p->m_tracks;
}

void TagEditorModel::populate(TagEditorDataPtr data)
{
    beginResetModel();
    p->reset();

    if(!data) {
        p->m_tracks.clear();
        p->m_fields.clear();
        endResetModel();
        return;
    }

    p->m_tracks = std::move(data->tracks);
    p->m_fields = std::move(data->fields);
    p->m_tags   = std::move(data->tags);

    for(const auto& key : data->tagOrder) {
        if(const auto it = p->m_tags.find(key); it != p->m_tags.end()) {
            p->m_root.appendChild(&it->second);
        }
    }

    p->m_root.sortCustomTags();

    endResetModel();
}

void TagEditorModel::setRatingRow(int row)
{
    p->m_ratingRow = row;
}

void TagEditorModel::updateTracks(const TrackList& tracks)
{
    Utils::updateCommonTracks(p->m_tracks, tracks, Utils::CommonOperation::Update);
}

void TagEditorModel::capitaliseRows(const QModelIndexList& rows)
{
    if(rows.empty() || p->m_tracks.empty()) {
        return;
    }

    std::set<int> uniqueRows;
    std::ranges::transform(rows, std::inserter(uniqueRows, uniqueRows.begin()), &QModelIndex::row);

    bool changed{false};

    for(const int row : uniqueRows) {
        if(row == p->m_ratingRow) {
            continue;
        }

        const QModelIndex valueIndex = index(row, 1, {});
        if(!valueIndex.isValid() || (flags(valueIndex) & Qt::ItemIsEditable) == 0) {
            continue;
        }

        auto* item = static_cast<TagEditorItem*>(valueIndex.internalPointer());
        if(!item) {
            continue;
        }

        if(item->valueChanged()) {
            if(item->splitTrackValues()) {
                changed |= updateItemTrackValues(
                    *item, capitaliseValues(splitEditorValues(item->changedValue(), p->multiValueSeparators())),
                    p->multiValueSeparators());
                continue;
            }

            if(item->field().multivalue) {
                changed |= item->setValue(joinEditorValues(capitaliseValues(splitEditorValues(
                                                               item->changedValue(), p->multiValueSeparators())),
                                                           p->multiValueSeparators()),
                                          p->multiValueSeparators());
                continue;
            }

            changed |= item->setValue(Utils::capitalise(item->changedValue()), p->multiValueSeparators());
            continue;
        }

        if(item->field().multivalue) {
            if(item->trackCount() > 1 && item->multipleValues()) {
                const QStringList trackValues
                    = capitalisedTrackValues(p->m_tracks, item->field(), p->multiValueSeparators());
                if(!trackValues.empty() && !hasDistinctValues(trackValues)) {
                    changed
                        |= updateItemTrackValues(*item, QStringList{trackValues.front()}, p->multiValueSeparators());
                }
                continue;
            }

            changed |= item->setValue(
                joinEditorValues(capitaliseValues(splitEditorValues(item->value(), p->multiValueSeparators())),
                                 p->multiValueSeparators()),
                p->multiValueSeparators());
            continue;
        }

        changed |= updateItemTrackValues(*item,
                                         capitalisedTrackValues(p->m_tracks, item->field(), p->multiValueSeparators()),
                                         p->multiValueSeparators());
    }

    if(changed) {
        invalidateData();
    }
}

void TagEditorModel::autoNumberTracks()
{
    const auto total = static_cast<int>(p->m_tracks.size());
    QStringList trackNums;

    for(int i{1}; i <= total; ++i) {
        trackNums.append(QString::number(i));
    }

    const auto addMissingTag = [this](const QString& tag) {
        if(!p->m_tags.contains(tag)) {
            TagEditorField field;
            field.name        = tag;
            field.scriptField = tag;

            auto* item = &p->m_tags.emplace(tag, TagEditorItem{field, &p->m_root}).first->second;
            p->m_root.appendChild(item);
        }
    };

    static const auto trackTag      = QString::fromLatin1(Constants::MetaData::TrackNumber);
    static const auto trackTotalTag = QString::fromLatin1(Constants::MetaData::TrackTotal);

    addMissingTag(trackTag);
    addMissingTag(trackTotalTag);

    auto& track = p->m_tags.at(trackTag);
    if(track.setValue(joinEditorValues(trackNums, p->multiValueSeparators()), p->multiValueSeparators())) {
        track.setMultipleValues(total > 1);
        track.setSplitTrackValues(true);
        invalidateData();
    }

    auto& totalTotal = p->m_tags.at(trackTotalTag);
    if(totalTotal.setValue(total)) {
        invalidateData();
    }
}

void TagEditorModel::updateValues(const std::map<QString, QString>& fieldValues, bool match)
{
    auto splitValues = [this](const QString& value) {
        QStringList values = splitMultiValueText(value, p->multiValueSeparators());
        values.resize(static_cast<qsizetype>(p->m_tracks.size()));
        return joinEditorValues(values, p->multiValueSeparators())
            .remove(QLatin1String{MultipleValuesPrefix})
            .trimmed();
    };

    auto updateItem = [&](TagEditorItem& item, const QString& value) {
        if(item.value() != value) {
            if(value.contains(QLatin1String{MultipleValuesPrefix})) {
                const QString splitValue = splitValues(value);
                item.setValue(splitValue, p->multiValueSeparators());
                item.setMultipleValues(p->m_tracks.size() > 1);
                item.setSplitTrackValues(true);
            }
            else {
                item.setValue(value, p->multiValueSeparators());
            }
        }
    };

    for(const auto& [tag, value] : fieldValues) {
        const auto tagIt
            = std::ranges::find_if(p->m_tags, [&tag](const auto& field) { return field.second.field().name == tag; });
        if(tagIt != p->m_tags.cend()) {
            updateItem(tagIt->second, value);
        }
        else if(match) {
            const int row = rowCount({});
            beginInsertRows({}, row, row);

            auto* item = &p->m_tags.emplace(tag, TagEditorItem{{}, &p->m_root}).first->second;
            item->setMultiValueSeparators(p->multiValueSeparators());
            item->setTitle(tag);
            item->addTrack(static_cast<int>(p->m_tracks.size()));
            updateItem(*item, value);
            item->setStatus(TagEditorItem::Added);
            p->m_root.appendChild(item);

            endInsertRows();
        }
    }

    invalidateData();
}

bool TagEditorModel::haveChanges()
{
    return std::ranges::any_of(p->m_tags, [](const auto& tag) { return tag.second.status() != TagEditorItem::None; });
}

bool TagEditorModel::haveOnlyStatChanges()
{
    auto isRatingField = [](const TagEditorItem& item) {
        const QString scriptField = item.field().scriptField;
        return scriptField.compare(QLatin1String{Constants::MetaData::RatingEditor}, Qt::CaseInsensitive) == 0
            || scriptField.compare(QLatin1String{Constants::MetaData::Rating}, Qt::CaseInsensitive) == 0
            || scriptField.compare(QLatin1String{Constants::MetaData::RatingNormalized}, Qt::CaseInsensitive) == 0
            || scriptField.compare(QLatin1String{Constants::MetaData::Stars}, Qt::CaseInsensitive) == 0
            || scriptField.compare(QLatin1String{Constants::MetaData::RatingStars}, Qt::CaseInsensitive) == 0;
    };

    bool changedRating{false};

    for(const auto& item : p->m_tags | std::views::values) {
        if(item.status() == TagEditorItem::None) {
            continue;
        }

        if(!isRatingField(item)) {
            return false;
        }

        changedRating = true;
    }

    return changedRating;
}

void TagEditorModel::applyChanges()
{
    QStringList fieldsToRemove;

    for(auto& [index, node] : p->m_tags) {
        const TagEditorItem::ItemStatus status = node.status();
        TagEditorField field                   = node.field();
        if(field.scriptField.isEmpty()) {
            field.scriptField = node.titleChanged() ? node.changedTitle() : node.title();
        }
        field.multivalue = shouldTreatAsMultiValue(field);
        field.isDefault  = !Track::isExtraTag(field.scriptField);

        switch(status) {
            case TagEditorItem::Added: {
                if(p->updateTrackMetadata(field, node.changedValue(), node.splitTrackValues())) {
                    node.applyChanges(field);
                }
                break;
            }
            case TagEditorItem::Removed: {
                if(p->updateTrackMetadata(field, {})) {
                    beginRemoveRows({}, node.row(), node.row());
                    p->m_root.removeChild(node.row());
                    endRemoveRows();
                    fieldsToRemove.push_back(node.title());
                }
                break;
            }
            case TagEditorItem::Changed: {
                if(node.titleChanged()) {
                    const auto fieldIt = std::ranges::find_if(std::as_const(p->m_tags), [node](const auto& tag) {
                        return tag.second.title() == node.title();
                    });
                    if(fieldIt != p->m_tags.end()) {
                        const QString changedTitle = node.changedTitle();
                        auto tagItem               = p->m_tags.extract(fieldIt);
                        if(p->updateTrackMetadata(field, {})) {
                            tagItem.key() = changedTitle;
                            p->m_tags.insert(std::move(tagItem));
                        }
                        field.scriptField = changedTitle;
                    }
                }

                if(p->updateTrackMetadata(field, node.valueChanged() ? node.changedValue() : node.value(),
                                          node.splitTrackValues())) {
                    node.applyChanges(field);
                    node.setSplitTrackValues(false);
                }
                break;
            }
            case TagEditorItem::None:
                break;
        }
    }

    for(const auto& field : fieldsToRemove) {
        p->m_tags.erase(field);
    }

    invalidateData();
}

QVariant TagEditorModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if(role == Qt::TextAlignmentRole) {
        return (Qt::AlignCenter);
    }

    if(role != Qt::DisplayRole || orientation == Qt::Orientation::Vertical) {
        return {};
    }

    switch(section) {
        case 0:
            return u"Name"_s;
        case 1:
            return u"Value"_s;
        default:
            break;
    }

    return {};
}

Qt::ItemFlags TagEditorModel::flags(const QModelIndex& index) const
{
    if(!index.isValid()) {
        return Qt::NoItemFlags;
    }

    auto flags = ExtendableTableModel::flags(index);

    if((index.column() != 0 || !index.data(TagEditorItem::IsDefault).toBool())) {
        flags |= Qt::ItemIsEditable;
    }

    return flags;
}

QVariant TagEditorModel::data(const QModelIndex& index, int role) const
{
    if(!checkIndex(index, CheckIndexOption::IndexIsValid)) {
        return {};
    }

    auto* item = static_cast<TagEditorItem*>(index.internalPointer());

    if(role == Qt::FontRole) {
        return item->font();
    }

    if(role == TagEditorItem::IsDefault) {
        return item->isDefault();
    }

    if(role == TagEditorItem::ScriptField) {
        const QString scriptField = item->field().scriptField;
        if(!scriptField.isEmpty()) {
            return scriptField;
        }
        return item->titleChanged() ? item->changedTitle() : item->title();
    }

    if(role == StarDelegate::Role::MixedValues) {
        return item->trackCount() > 1 && item->multipleValues();
    }

    if(role == Qt::DisplayRole || role == Qt::EditRole || role == TagEditorItem::Title) {
        if(index.column() == 0) {
            const QString title = item->titleChanged() ? item->changedTitle() : item->title();
            if(role == Qt::EditRole) {
                return title;
            }
            if(!item->isSetField() && role != TagEditorItem::Title) {
                const QString name = u"<"_s + title + u">"_s;
                return name;
            }

            return title;
        }

        if(index.row() == p->m_ratingRow) {
            const bool mixedValues = index.data(StarDelegate::Role::MixedValues).toBool();
            return QVariant::fromValue(StarRating{
                mixedValues ? 0.0F : (item->valueChanged() ? item->changedValue().toFloat() : item->value().toFloat()),
                5, p->m_settings->value<Settings::Gui::StarRatingSize>()});
        }

        if(role == Qt::EditRole) {
            if(item->valueChanged()) {
                return item->changedValue();
            }
            if(item->trackCount() > 1 && item->multipleValues()) {
                return item->displayValue();
            }
            return item->value();
        }

        return item->valueChanged() ? item->changedDisplayValue() : item->displayValue();
    }

    if(role == Qt::ToolTipRole && index.row() == p->m_ratingRow
       && index.data(StarDelegate::Role::MixedValues).toBool()) {
        return tr("Multiple values. Choose a rating to apply it to all selected tracks.");
    }

    return {};
}

bool TagEditorModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
    if(p->m_tracks.empty()) {
        return false;
    }

    if(role != Qt::EditRole) {
        return false;
    }

    auto* item = static_cast<TagEditorItem*>(index.internalPointer());

    switch(index.column()) {
        case 0: {
            const QString newTitle = value.toString().toUpper();

            if((item->status() == TagEditorItem::Added && newTitle == p->m_defaultFieldtext)
               || p->hasTagConflict(item, newTitle)) {
                if(item->status() == TagEditorItem::Added) {
                    Q_EMIT pendingRowCancelled();
                }
                return false;
            }

            if(!item->setTitle(newTitle)) {
                if(item->status() == TagEditorItem::Added) {
                    Q_EMIT pendingRowCancelled();
                }
                return false;
            }

            break;
        }
        case 1: {
            QString setValue = value.toString();

            if(index.row() == p->m_ratingRow) {
                const auto rating = value.value<StarRating>();
                setValue          = rating.rating() == 0 ? QString{} : QString::number(rating.rating());
            }
            else if(setValue.startsWith(QLatin1StringView{MultipleValuesPrefix})) {
                return false;
            }

            if(!item->setValue(setValue, p->multiValueSeparators())) {
                return false;
            }
            break;
        }
        default:
            break;
    }

    Q_EMIT dataChanged(index, index);

    return true;
}

QModelIndex TagEditorModel::index(int row, int column, const QModelIndex& parent) const
{
    if(!hasIndex(row, column, parent)) {
        return {};
    }

    TagEditorItem* item = p->m_root.child(row);

    return createIndex(row, column, item);
}

int TagEditorModel::rowCount(const QModelIndex& /*parent*/) const
{
    return p->m_root.childCount();
}

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

bool TagEditorModel::removeRows(int row, int count, const QModelIndex& /*parent*/)
{
    for(int i{row}; i < row + count; ++i) {
        const QModelIndex& index = this->index(i, 0, {});

        if(!index.isValid()) {
            return false;
        }

        auto* item = static_cast<TagEditorItem*>(index.internalPointer());
        if(!item) {
            continue;
        }

        if(item->isSetField()) {
            setData(index.siblingAtColumn(1), {}, Qt::EditRole);
            continue;
        }

        if(item->status() == TagEditorItem::Added) {
            beginRemoveRows({}, i, i);
            p->m_root.removeChild(i);
            endRemoveRows();
            p->m_tags.erase(p->m_defaultFieldtext + QString::number(row));
        }
        else {
            item->setStatus(TagEditorItem::Removed);
            Q_EMIT dataChanged(index, index.siblingAtColumn(columnCount({}) - 1), {Qt::FontRole});
        }
    }

    return true;
}

void TagEditorModel::addPendingRow()
{
    TagEditorField field;
    field.name = p->m_defaultFieldtext;
    TagEditorItem newItem{field, &p->m_root};
    newItem.setStatus(TagEditorItem::Added);

    const int row = p->m_root.childCount();
    auto* item    = &p->m_tags.emplace(p->m_defaultFieldtext + QString::number(row), newItem).first->second;

    beginInsertRows({}, row, row);
    p->m_root.appendChild(item);
    endInsertRows();
}

void TagEditorModel::removePendingRow()
{
    const int row = rowCount({}) - 1;
    beginRemoveRows({}, row, row);
    p->m_root.removeChild(row);
    endRemoveRows();
}
} // namespace Fooyin::TagEditor

#include "moc_tageditormodel.cpp"
