/* Playlist.cpp */

/* Copyright (C) 2011-2024 Michael Lugmair (Lucio Carreras)
 *
 * This file is part of sayonara player
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include "Playlist.h"
#include "PlaylistModifiers.h"
#include "PlaylistShuffleHistory.h"
#include "PlaylistStopBehavior.h"

#include "Components/PlayManager/PlayManager.h"
#include "Components/Tagging/ChangeNotifier.h"
#include "Utils/Algorithm.h"
#include "Utils/FileUtils.h"
#include "Utils/FileSystem.h"
#include "Utils/MetaData/MetaDataList.h"
#include "Utils/Settings/Settings.h"

#include <QMultiHash>
#include <tuple>
#include <optional>

namespace Playlist
{
	namespace
	{
		QMultiHash<QString, int> createFilepathIndexHash(const MetaDataList& tracks)
		{
			auto result = QMultiHash<QString, int>();

			auto i = 0;
			for(auto it = tracks.begin(); it != tracks.end(); ++it, ++i)
			{
				result.insert(it->filepath(), i);
			}

			return result;
		}

		int getNextTrackIndex(const int currentTrackIndex, const int trackCount, const PlaylistMode& playlistMode,
		                      const ShuffleHistory& shuffleHistory)
		{
			if(trackCount < 0)
			{
				return -1;
			}

			const auto rep1 = PlaylistMode::isActiveAndEnabled(playlistMode.rep1());
			const auto shuffle = PlaylistMode::isActiveAndEnabled(playlistMode.shuffle());
			const auto repAll = (PlaylistMode::isActiveAndEnabled(playlistMode.repAll()));
			const auto isLastTrack = (currentTrackIndex == trackCount - 1);

			if((currentTrackIndex == -1) && !shuffle)
			{
				return 0;
			}

			if(rep1)
			{
				return currentTrackIndex;
			}

			if(shuffle)
			{
				return shuffleHistory.nextTrackIndex(repAll);
			}

			if(isLastTrack)
			{
				return repAll ? 0 : -1;
			}

			return currentTrackIndex + 1;
		}

		QList<int> findTracks(const MetaDataList& tracks, const QString& path)
		{
			if(path.isEmpty())
			{
				return {};
			}

			QList<int> ret;
			for(auto it = tracks.begin(); it != tracks.end(); ++it)
			{
				if(it->filepath() == path)
				{
					ret << static_cast<int>(std::distance(tracks.begin(), it));
				}
			}

			return ret;
		}

		bool checkIfOperationIsPossible(const Reason reason, const Operation operation)
		{
			using Condition = std::tuple<Reason, Operation, std::optional<bool>>;
			const auto allowedOperations = QList<Condition>{
				{Reason::DynamicPlayback, Operation::Append, GetSetting(Set::PL_ModificatorAllowDynamicPlayback)},
				{Reason::ReloadFromDatabase, Operation::Rebuild, {}},
				{Reason::StreamHistory, Operation::Insert, {}},
				{Reason::TracksDeleted, Operation::Remove, {}},
				{Reason::UserInterface, Operation::Arrange, GetSetting(Set::PL_ModificatorAllowRearrangeMethods)},
				{Reason::UserInterface, Operation::EnableAll, {}},
			};

			return Util::Algorithm::contains(allowedOperations, [reason, operation](const auto allowed) {
				const auto settingCondition = std::get<2>(allowed);
				return (std::get<Reason>(allowed) == reason) &&
				       (std::get<Operation>(allowed) == operation) &&
				       (!settingCondition.has_value() || (settingCondition.value() == true));
			});
		}

		bool trackExists(const QString& filepath, const Util::FileSystemPtr& fileSystem)
		{
			return fileSystem->exists(filepath) || Util::File::isWWW(filepath);
		}
	}

	struct Playlist::Private
	{
		Tagging::ChangeNotifier* metadataChangeNotifier{Tagging::ChangeNotifier::instance()};
		PlayManager* playManager;
		Util::FileSystemPtr fileSystem;
		MetaDataList tracks;
		UniqueId lastTrackBeforeStopUniqueId{0U};
		ShuffleHistory shuffleHistory;
		StopBehavior stopBehavior;
		UniqueId playingUniqueId{0U};
		PlaylistMode playlistMode;
		int playlistIndex;
		bool playlistChanged{false};
		bool busy{false};

		Private(const int playlistIndex, const PlaylistMode& playlistMode, PlayManager* playManager,
		        Util::FileSystemPtr fileSystem, Playlist* playlist) :
			playManager(playManager),
			fileSystem{std::move(fileSystem)},
			shuffleHistory(playlist),
			stopBehavior(playlist),
			playlistMode(playlistMode),
			playlistIndex(playlistIndex) {}
	};

	Playlist::Playlist(const int playlistIndex, const QString& name, PlayManager* playManager,
	                   const Util::FileSystemPtr& fileSystem) :
		DBInterface(name),
		m{
			Pimpl::make<Private>(
				playlistIndex, GetSetting(Set::PL_Mode), playManager, fileSystem, this)
		}
	{
		connect(m->metadataChangeNotifier,
		        &Tagging::ChangeNotifier::sigMetadataChanged,
		        this,
		        &Playlist::metadataChanged);
		connect(m->metadataChangeNotifier,
		        &Tagging::ChangeNotifier::sigMetadataDeleted,
		        this,
		        &Playlist::metadataDeleted);

		connect(m->playManager, &PlayManager::sigCurrentMetadataChanged, this, &Playlist::currentMetadataChanged);
		connect(m->playManager, &PlayManager::sigDurationChangedMs, this, &Playlist::durationChanged);

		ListenSetting(Set::PL_Mode, Playlist::settingPlaylistModeChanged);
	}

	Playlist::~Playlist() = default;

	int Playlist::findFirstValidIndex(int index) const
	{
		while(Util::between(index, m->tracks))
		{
			if(trackExists(m->tracks[index].filepath(), m->fileSystem))
			{
				return index;
			}

			m->tracks[index].setDisabled(true);
			index++;
		}

		return -1;
	}

	bool Playlist::prepareTrack(const int index)
	{
		if(index < 0)
		{
			stop();

			m->playingUniqueId = 0;
			m->stopBehavior.clear();

			return false;
		}

		const auto oldIndex = findCurrentTrackIndex();
		const auto& track = m->tracks[index];

		m->playingUniqueId = track.uniqueId();
		m->shuffleHistory.addTrack(m->playingUniqueId);
		m->playManager->changeCurrentTrack(track, index);
		SetSetting(Set::PL_LastPlaylist, id());

		emit sigTrackChanged(oldIndex, findCurrentTrackIndex());

		return true;
	}

	bool Playlist::changeTrack(int index, const MilliSeconds positionMs)
	{
		index = findFirstValidIndex(index);

		if(const auto isCurrentTrack = (index >= 0) && (findCurrentTrackIndex() == index); isCurrentTrack)
		{
			m->playManager->seekAbsoluteMs(positionMs);
			return true;
		}

		const auto prepared = prepareTrack(index);
		if(prepared)
		{
			if(positionMs > 0)
			{
				m->playManager->playAndSeek(positionMs);
			}
			else
			{
				m->playManager->play();
			}
		}

		return prepared;
	}

	void Playlist::metadataDeleted()
	{
		const auto deletedTracks = m->metadataChangeNotifier->deletedMetadata();
		Util::Algorithm::removeIf(m->tracks, [&](const auto& track) {
			return Util::Algorithm::contains(deletedTracks, [&](const auto& tmpTrack) {
				return track.isEqual(tmpTrack);
			});
		});

		emit sigItemsChanged(index());
	}

	void Playlist::metadataChanged()
	{
		const auto filepathIndexMap = createFilepathIndexHash(tracks());

		const auto& changedTracks = m->metadataChangeNotifier->changedMetadata();
		for(const auto& [oldTrack, newTrack]: changedTracks)
		{
			const auto indexes = filepathIndexMap.values(oldTrack.filepath());
			for(const auto& index: indexes)
			{
				replaceTrack(index, newTrack);
			}
		}

		emit sigItemsChanged(this->index());
	}

	void Playlist::currentMetadataChanged()
	{
		const auto currentTrack = m->playManager->currentTrack();
		const auto indexList = findTracks(m->tracks, currentTrack.filepath());
		for(const auto index: indexList)
		{
			replaceTrack(index, currentTrack);
		}
	}

	void Playlist::durationChanged()
	{
		const auto currentTrack = m->playManager->currentTrack();
		const auto indexList = findTracks(m->tracks, currentTrack.filepath());

		for(const auto index: indexList)
		{
			auto track = (m->tracks[index]);
			track.setDurationMs(std::max<MilliSeconds>(0, currentTrack.durationMs()));
			replaceTrack(index, track);
		}
	}

	void Playlist::replaceTrack(const int index, const MetaData& track)
	{
		if(!Util::between(index, m->tracks) || (m->tracks[index].isDisabled()))
		{
			return;
		}

		const auto oldUniqueId = m->tracks[index].uniqueId();
		const auto isCurrent = (oldUniqueId == m->playingUniqueId);
		const auto isEnabled = trackExists(track.filepath(), m->fileSystem) && !track.isDisabled();

		m->shuffleHistory.replaceTrack(m->tracks[index], track);
		m->tracks[index] = track;
		m->tracks[index].setDisabled(!isEnabled);

		if(isCurrent)
		{
			m->playingUniqueId = m->tracks[index].uniqueId();
		}

		emit sigItemsChanged(this->index());
	}

	void Playlist::play()
	{
		if(findCurrentTrackIndex() < 0)
		{
			next();
		}
	}

	void Playlist::stop()
	{
		m->shuffleHistory.clear();
		m->lastTrackBeforeStopUniqueId = 0U;

		const auto currentTrack = findCurrentTrackIndex();
		if(currentTrack >= 0)
		{
			m->stopBehavior.setTrackIndexBeforeStop(currentTrack);
			m->playingUniqueId = 0;
		}
		else
		{
			m->stopBehavior.clear();
		}

		emit sigTrackChanged(currentTrack, -1);
	}

	void Playlist::previous()
	{
		constexpr const auto minimalOffset = 2000;
		if(m->playManager->currentPositionMs() > minimalOffset)
		{
			m->playManager->seekAbsoluteMs(0);
			return;
		}

		if(PlaylistMode::isActiveAndEnabled(m->playlistMode.shuffle()))
		{
			if(const auto index = m->shuffleHistory.previousShuffleIndex(); index >= 0)
			{
				m->shuffleHistory.popBack();
				changeTrack(index);
				m->shuffleHistory.popBack();
				return;
			}
		}

		m->shuffleHistory.clear();
		changeTrack(findCurrentTrackIndex() - 1);
	}

	void Playlist::next()
	{
		const auto nextIndex = getNextTrackIndex(findCurrentTrackIndex(), count(), m->playlistMode, m->shuffleHistory);
		changeTrack(nextIndex);
	}

	bool Playlist::continueFromStop()
	{
		const auto trackIndexBeforeStop = m->stopBehavior.trackIndexBeforeStop();
		if(const auto trackChanged = changeTrack(trackIndexBeforeStop); !trackChanged)
		{
			next();
		}

		return true;
	}

	bool Playlist::canContinueFromStop() const
	{
		return (m->stopBehavior.trackIndexBeforeStop() >= 0);
	}

	void Playlist::toggleStopAfterTrack(const int index)
	{
		if(Util::between(index, m->tracks))
		{
			const auto uniqueId = m->tracks[index].uniqueId();
			m->lastTrackBeforeStopUniqueId = (m->lastTrackBeforeStopUniqueId == uniqueId) ? 0U : uniqueId;
		}
	}

	int Playlist::findLastIndexBeforeStop() const
	{
		return Util::Algorithm::indexOf(m->tracks, [uniqueId = m->lastTrackBeforeStopUniqueId](const auto& track) {
			return (track.uniqueId() == uniqueId);
		});
	}

	bool Playlist::isLastTrackBeforeStop(const int index) const
	{
		return Util::between(index, count()) && (m->tracks[index].uniqueId() == m->lastTrackBeforeStopUniqueId);
	}

	int Playlist::createPlaylist(const MetaDataList& tracks)
	{
		if(const auto append = PlaylistMode::isActiveAndEnabled(m->playlistMode.append()); !append)
		{
			m->tracks.clear();
			m->shuffleHistory.clear();
		}

		m->tracks << tracks;
		setChanged(true);

		return count();
	}

	int Playlist::index() const { return m->playlistIndex; }

	void Playlist::setIndex(const int index) { m->playlistIndex = index; }

	PlaylistMode Playlist::mode() const { return m->playlistMode; }

	void Playlist::setMode(const PlaylistMode& mode)
	{
		if(m->playlistMode.shuffle() != mode.shuffle())
		{
			m->shuffleHistory.clear();
		}

		m->playlistMode = mode;
	}

	int Playlist::count() const { return m->tracks.count(); }

	bool Playlist::isBusy() const { return m->busy; }

	void Playlist::setBusy(const bool busy)
	{
		m->busy = busy;
		emit sigBusyChanged(busy);
	}

	int Playlist::findCurrentTrackIndex() const
	{
		if(m->playingUniqueId == 0)
		{
			return -1;
		}

		return Util::Algorithm::indexOf(m->tracks, [&](const auto& track) {
			return (track.uniqueId() == m->playingUniqueId);
		});
	}

	bool Playlist::isCurrentTrack(const int index) const
	{
		return Util::between(index, count()) && (m->tracks[index].uniqueId() == m->playingUniqueId);
	}

	bool Playlist::wasChanged() const { return m->playlistChanged; }

	void Playlist::setChanged(const bool b)
	{
		m->playlistChanged = b;
		emit sigItemsChanged(m->playlistIndex);
	}

	void Playlist::emitLockChanged()
	{
		emit sigLockChanged();
	}

	void Playlist::resetChangedStatus()
	{
		setChanged(false);
	}

	void Playlist::settingPlaylistModeChanged()
	{
		setMode(GetSetting(Set::PL_Mode));
	}

	const MetaDataList& Playlist::tracks() const { return m->tracks; }

	void Playlist::modifyTracks(Modificator&& modificator, const Reason reason, const Operation operation)
	{
		if(isLocked() && !checkIfOperationIsPossible(reason, operation))
		{
			return;
		}

		m->tracks = modificator(std::move(m->tracks));
		setChanged(true);
	}
}
