/*
 * Fooyin
 * Copyright © 2026, 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 <core/playlist/playlistchangeset.h>

#include <algorithm>
#include <ranges>
#include <unordered_map>
#include <utility>

constexpr auto ResetThreshold = 500;

namespace Fooyin {
namespace {
using TrackEntryList = std::vector<UId>;

TrackEntryList playlistTrackEntries(const PlaylistTrackList& tracks)
{
    TrackEntryList result;
    result.reserve(tracks.size());
    std::ranges::transform(tracks, std::back_inserter(result), &PlaylistTrack::entryId);
    return result;
}

std::optional<std::vector<PlaylistTrackMove>> buildPlaylistMoves(TrackEntryList currentEntries,
                                                                 const TrackEntryList& newEntries)
{
    if(currentEntries.size() != newEntries.size()) {
        return {};
    }

    std::vector<PlaylistTrackMove> result;

    for(int targetIndex{0}; std::cmp_less(targetIndex, newEntries.size()); ++targetIndex) {
        if(currentEntries.at(static_cast<size_t>(targetIndex)) == newEntries.at(static_cast<size_t>(targetIndex))) {
            continue;
        }

        const auto sourceIt = std::ranges::find(currentEntries.begin() + targetIndex, currentEntries.end(),
                                                newEntries.at(static_cast<size_t>(targetIndex)));
        if(sourceIt == currentEntries.end()) {
            return {};
        }

        result.push_back({.entryId = *sourceIt, .targetIndex = targetIndex});

        std::rotate(currentEntries.begin() + targetIndex, sourceIt, sourceIt + 1);
    }

    return result;
}
} // namespace

std::optional<PlaylistChangeset> buildPlaylistChangeset(const PlaylistTrackList& oldTracks,
                                                        const PlaylistTrackList& newTracks,
                                                        const TrackEntryIdSet& updatedTrackEntries)
{
    PlaylistChangeset result;
    const TrackEntryList newTrackEntries = playlistTrackEntries(newTracks);

    const auto ensureUniqueEntries = [](const PlaylistTrackList& tracks) {
        TrackEntryIdSet seenEntries;
        seenEntries.reserve(tracks.size());

        for(const auto& track : tracks) {
            if(!track.entryId.isValid() || !seenEntries.emplace(track.entryId).second) {
                return false;
            }
        }
        return true;
    };

    if(!ensureUniqueEntries(oldTracks) || !ensureUniqueEntries(newTracks)) {
        return {};
    }

    std::unordered_map<UId, int, UId::UIdHash> newTrackIndexes;
    newTrackIndexes.reserve(newTracks.size());

    for(int newIndex{0}; const auto& track : newTracks) {
        newTrackIndexes.emplace(track.entryId, newIndex++);
    }

    TrackEntryList retainedTrackEntries;
    retainedTrackEntries.reserve(std::min(oldTracks.size(), newTracks.size()));

    TrackEntryIdSet oldEntrySet;
    oldEntrySet.reserve(oldTracks.size());

    for(const auto& track : oldTracks) {
        oldEntrySet.emplace(track.entryId);
    }

    TrackEntryIdSet newEntrySet;
    newEntrySet.reserve(newTracks.size());
    for(const auto& track : newTracks) {
        newEntrySet.emplace(track.entryId);
    }

    for(const auto& oldTrack : oldTracks) {
        if(!newEntrySet.contains(oldTrack.entryId)) {
            result.removedEntries.emplace_back(oldTrack.entryId);
            continue;
        }

        const int newIndex = newTrackIndexes.at(oldTrack.entryId);
        retainedTrackEntries.emplace_back(oldTrack.entryId);

        if(!oldTrack.track.sameDataAs(newTracks.at(static_cast<size_t>(newIndex)).track)) {
            result.updatedEntries.emplace_back(oldTrack.entryId);
        }
        else if(updatedTrackEntries.contains(oldTrack.entryId)) {
            result.updatedEntries.emplace_back(oldTrack.entryId);
        }
    }

    result.replacesAllEntries = !oldTracks.empty() && retainedTrackEntries.empty();

    PlaylistTrackInsertion insertion;
    for(int newIndex{0}; const auto& track : newTracks) {
        const bool isNewTrack = !oldEntrySet.contains(track.entryId);
        if(isNewTrack) {
            if(insertion.index < 0) {
                insertion.index = newIndex;
            }
            insertion.tracks.push_back(track);
        }
        else if(insertion.isValid()) {
            result.insertions.push_back(std::move(insertion));
            insertion = {};
        }
        ++newIndex;
    }
    if(insertion.isValid()) {
        result.insertions.push_back(std::move(insertion));
    }

    TrackEntryList currentEntries{retainedTrackEntries};
    for(const auto& groupedInsertion : result.insertions) {
        const TrackEntryList insertionEntries = playlistTrackEntries(groupedInsertion.tracks);
        currentEntries.insert(currentEntries.begin() + groupedInsertion.index, insertionEntries.cbegin(),
                              insertionEntries.cend());
    }

    if(const auto moves = buildPlaylistMoves(std::move(currentEntries), newTrackEntries)) {
        result.moves = *moves;
    }
    else {
        return {};
    }

    std::ranges::sort(result.updatedEntries);
    result.updatedEntries.erase(std::ranges::unique(result.updatedEntries).begin(), result.updatedEntries.end());

    int insertedTrackCount{0};
    for(const auto& insertionGroup : result.insertions) {
        insertedTrackCount += static_cast<int>(insertionGroup.tracks.size());
    }

    const int changedTrackCount = static_cast<int>(result.removedEntries.size()) + insertedTrackCount
                                + static_cast<int>(result.moves.size())
                                + static_cast<int>(result.updatedEntries.size());
    const int baselineTrackCount
        = static_cast<int>(oldTracks.size() > newTracks.size() ? oldTracks.size() : newTracks.size());

    if(changedTrackCount > ResetThreshold || changedTrackCount > (baselineTrackCount / 2)) {
        result.requiresReset = true;
    }

    return result;
}
} // namespace Fooyin
