/* 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/>.
 *
 *  Created on: Apr 6, 2011
 *      Author: Michael Lugmair (Lucio Carreras)
 */

#include "PlaylistHandler.h"
#include "LocalPathPlaylistCreator.h"
#include "Playlist.h"
#include "PlaylistChangeNotifier.h"
#include "PlaylistLoader.h"
#include "PlaylistModifiers.h"
#include "PlaylistSaver.h"

#include "PlayManager/PlayManager.h"
#include "Utils/Algorithm.h"
#include "Utils/FileSystem.h"
#include "Utils/Logger/Logger.h"
#include "Utils/MetaData/MetaDataList.h"
#include "Utils/Playlist/CustomPlaylist.h"
#include "Utils/Set.h"
#include "Utils/Settings/Settings.h"

#include <cassert>

namespace Playlist
{
	namespace
	{
		void resetLastPlayedTrack(PlayManager* playManager)
		{
			if(playManager->initialPositionMs() > 0)
			{
				playManager->stop();
			}

			playManager->setCurrentPositionMs(0);
		}

		bool isPlaylistOnlyValidPlaylist(Accessor* playlistAccessor, const int index)
		{
			return (index == 1) &&
			       (playlistAccessor->count() == 2) &&
			       (playlistAccessor->playlist(0)->tracks().isEmpty()) &&
			       (playlistAccessor->playlist(0)->isTemporary());
		}

		bool isPlaylistReadyForPlayback(const PlaylistPtr& playlist)
		{
			return playlist && (playlist->count() > 0);
		}
	}

	struct Handler::Private
	{
		QList<PlaylistPtr> playlists;
		PlayManager* playManager;
		Util::FileSystemPtr fileSystem;
		int currentPlaylistIndex {-1};

		explicit Private(PlayManager* playManager, Util::FileSystemPtr fileSystem) :
			playManager {playManager},
			fileSystem {std::move(fileSystem)} {}

		void initPlaylists(Handler* handler, const std::shared_ptr<Loader>& playlistLoader) const
		{
			spLog(Log::Debug, this) << "Loading playlists...";

			const auto& loadedPlaylistst = playlistLoader->playlists();
			if(loadedPlaylistst.isEmpty())
			{
				handler->createEmptyPlaylist();
				return;
			}

			for(const auto& playlist: loadedPlaylistst)
			{
				handler->createPlaylist(playlist);
			}

			const auto lastIndex = playlistLoader->getLastPlaylistIndex();
			const auto currentIndex = std::max(0, lastIndex);
			handler->setCurrentIndex(currentIndex);

			const auto lastTrackIndex = playlistLoader->getLastTrackIndex();
			if(lastTrackIndex >= 0)
			{
				auto lastPlaylist = handler->playlist(currentIndex);
				lastPlaylist->prepareTrack(lastTrackIndex);
			}

			else
			{
				playManager->stop();
			}
		}
	};

	Handler::Handler(PlayManager* playManager, const std::shared_ptr<Loader>& playlistLoader,
	                 const Util::FileSystemPtr& fileSystem) :
		m {Pimpl::make<Private>(playManager, fileSystem)}
	{
		m->initPlaylists(this, playlistLoader);

		connect(m->playManager, &PlayManager::sigPlaystateChanged, this, &Handler::playstateChanged);
		connect(m->playManager, &PlayManager::sigNext, this, &Handler::next);
		connect(m->playManager, &PlayManager::sigContinueFromStop, this, &Handler::continueFromStop);
		connect(m->playManager, &PlayManager::sigPrevious, this, &Handler::previous);
		connect(m->playManager, &PlayManager::sigStreamFinished, this, &Handler::wwwTrackFinished);

		auto* playlistChangeNotifier = ChangeNotifier::instance();
		connect(playlistChangeNotifier, &ChangeNotifier::sigPlaylistRenamed, this, &Handler::playlistRenamed);
		connect(playlistChangeNotifier, &ChangeNotifier::sigPlaylistDeleted, this, &Handler::playlistDeleted);
	}

	Handler::~Handler() = default;

	void Handler::shutdown()
	{
		for(auto i = count() - 1; i >= 0; i--)
		{
			if(playlist(i)->tracks().isEmpty())
			{
				closePlaylist(i);
			}
		}

		saveCurrentPlaylists(m->playlists);

		m->playlists.clear();
		m->currentPlaylistIndex = -1;
	}

	int Handler::addNewPlaylist(const QString& name, const bool temporary, const bool overwriteLocked)
	{
		if(const auto index = exists(name); index >= 0)
		{
			const auto& playlist = m->playlists[index];
			if(!playlist->isLocked() || overwriteLocked)
			{
				return index;
			}
		}

		const auto newName = name.isEmpty()
		                     ? requestNewPlaylistName()
		                     : name;

		auto playlist = std::make_shared<Playlist>(m->playlists.count(), newName, m->playManager, m->fileSystem);
		playlist->setTemporary(temporary);

		m->playlists.push_back(playlist);

		emit sigNewPlaylistAdded(m->playlists.count() - 1);
		emit sigActivePlaylistChanged(activeIndex());

		connect(playlist.get(), &Playlist::sigTrackChanged, this, &Handler::trackChanged);

		return playlist->index();
	}

	int
	Handler::createPlaylist(const MetaDataList& tracks, const QString& name, const bool temporary, const bool isLocked)
	{
		const auto overwriteLocked = isLocked; // a locked playlist might overwrite another locked playlist
		const auto index = addNewPlaylist(name, temporary, overwriteLocked);

		auto& playlist = m->playlists[index];
		if(!playlist->isBusy())
		{
			playlist->createPlaylist(tracks);
			playlist->setTemporary(playlist->isTemporary() && temporary);
			playlist->setLocked(isLocked);
			if(playlist->isTemporary())
			{
				playlist->save();
			}
		}

		setCurrentIndex(index);
		return m->currentPlaylistIndex;
	}

	int Handler::createPlaylist(const CustomPlaylist& playlist)
	{
		const auto index = createPlaylist(playlist.tracks(), playlist.name(), playlist.isTemporary());

		auto& createdPlaylist = m->playlists[index];
		createdPlaylist->setId(playlist.id());
		createdPlaylist->setChanged(false);
		createdPlaylist->setLocked(playlist.isLocked());

		return m->currentPlaylistIndex;
	}

	int Handler::createPlaylist(const QStringList& paths, const QString& name, const bool temporary,
	                            LocalPathPlaylistCreator* playlistFromPathCreator)
	{
		if(!playlistFromPathCreator)
		{
			playlistFromPathCreator = LocalPathPlaylistCreator::create(this);
		}

		connect(playlistFromPathCreator, &LocalPathPlaylistCreator::sigAllPlaylistsCreated,
		        playlistFromPathCreator, &QObject::deleteLater);

		playlistFromPathCreator->createPlaylists(paths, name, temporary);

		if(const auto createdPlaylists = playlistFromPathCreator->createdPlaylists(); !createdPlaylists.isEmpty())
		{
			const auto [playlistIndex, row] = createdPlaylists[0];
			setCurrentIndex(playlistIndex);
		}

		return m->currentPlaylistIndex;
	}

	int Handler::createCommandLinePlaylist(const QStringList& paths, LocalPathPlaylistCreator* playlistFromPathCreator)
	{
		if(!playlistFromPathCreator)
		{
			playlistFromPathCreator = LocalPathPlaylistCreator::create(this);
		}

		connect(playlistFromPathCreator, &LocalPathPlaylistCreator::sigAllPlaylistsCreated,
		        this, &Handler::onCommandLinePlaylistsCreated);

		const auto maybeName = filesystemPlaylistName();
		const auto playlistName = maybeName.isEmpty()
		                          ? requestNewPlaylistName()
		                          : maybeName;

		playlistFromPathCreator->createPlaylists(paths, playlistName, true);

		return m->currentPlaylistIndex;
	}

	void Handler::onCommandLinePlaylistsCreated()
	{
		const auto* playlistFromPathCreator = dynamic_cast<LocalPathPlaylistCreator*>(sender());
		const auto createdPlaylists = playlistFromPathCreator->createdPlaylists();

		assert(!createdPlaylists.isEmpty());

		const auto [playlistIndex, row] = createdPlaylists[0];

		resetLastPlayedTrack(m->playManager);
		playlist(playlistIndex)->prepareTrack(row);
		setCurrentIndex(playlistIndex);
		if(isPlaylistOnlyValidPlaylist(this, playlistIndex))
		{
			closePlaylist(0);
		}

		sender()->deleteLater();
	}

	int Handler::createEmptyPlaylist(const bool override)
	{
		const auto name = override ? QString() : requestNewPlaylistName();
		return createPlaylist(MetaDataList(), name, true);
	}

	void Handler::playstateChanged(const PlayState state)
	{
		if(state == PlayState::Playing)
		{
			if(auto activePlaylist = determineActivePlaylist(); activePlaylist)
			{
				activePlaylist->play();
			}
		}

		else if(state == PlayState::Stopped)
		{
			for(auto& playlist: m->playlists)
			{
				playlist->stop();
			}
		}
	}

	void Handler::next()
	{
		if(auto activePlaylist = determineActivePlaylist(); activePlaylist)
		{
			const auto currentIndex = activePlaylist->findCurrentTrackIndex();
			const auto lastIndexBeforeStop = activePlaylist->findLastIndexBeforeStop();
			const auto stopAfterCurrentTrack = (currentIndex == lastIndexBeforeStop) && (currentIndex >= 0);
			if(stopAfterCurrentTrack)
			{ // if we actively call stop() here inside the PlaylistHandler, the StopBehavior is taken into account
				activePlaylist->stop();
			}

			else
			{
				activePlaylist->next();
			}

			if(activePlaylist->findCurrentTrackIndex() < 0)
			{
				m->playManager->stop();
			}
		}
	}

	void Handler::previous()
	{
		if(auto activePlaylist = playlist(activeIndex()); activePlaylist)
		{
			activePlaylist->previous();
		}
	}

	void Handler::continueFromStop()
	{
		if(GetSetting(Set::PL_RememberTrackAfterStop))
		{
			const auto activeIndex = Util::Algorithm::indexOf(m->playlists, [](const auto& playlist) {
				return playlist->canContinueFromStop();
			});

			const auto activePlaylist = playlist(activeIndex);
			if(activePlaylist && activePlaylist->continueFromStop())
			{
				return;
			}
		}

		next();
	}

	void Handler::trackChanged(const int /*oldIndex*/, const int /*newIndex*/)
	{
		auto* playlist = dynamic_cast<Playlist*>(sender());
		if(playlist->findCurrentTrackIndex() >= 0)
		{
			for(const auto& playlistPtr: m->playlists)
			{
				if(playlist->index() != playlistPtr->index())
				{
					playlistPtr->stop();
				}
			}
		}
	}

	int Handler::activeIndex() const
	{
		return Util::Algorithm::indexOf(m->playlists, [](const auto& playlist) {
			return (playlist->findCurrentTrackIndex() >= 0);
		});
	}

	int Handler::currentIndex() const
	{
		return m->currentPlaylistIndex;
	}

	void Handler::setCurrentIndex(int playlistIndex)
	{
		if(Util::between(playlistIndex, m->playlists))
		{
			if(m->currentPlaylistIndex != playlistIndex)
			{
				m->currentPlaylistIndex = playlistIndex;
				emit sigCurrentPlaylistChanged(playlistIndex);
			}
		}
	}

	int Handler::count() const { return m->playlists.size(); }

	QString Handler::requestNewPlaylistName(const QString& prefix) const { return requestNewDatabaseName(prefix); }

	void Handler::closePlaylist(const int playlistIndex)
	{
		if(!Util::between(playlistIndex, m->playlists))
		{
			return;
		}

		if(auto playlist = this->playlist(playlistIndex); playlist && playlist->isTemporary())
		{
			playlist->deletePlaylist();
		}

		m->playlists.removeAt(playlistIndex);
		emit sigPlaylistClosed(playlistIndex);

		for(const auto& remaningPlaylist: m->playlists)
		{
			if((remaningPlaylist->index() >= playlistIndex) &&
			   (remaningPlaylist->index() > 0))
			{
				remaningPlaylist->setIndex(remaningPlaylist->index() - 1);
			}
		}

		if(m->playlists.isEmpty())
		{
			const auto newIndex = addNewPlaylist(requestNewPlaylistName(), true, false);
			setCurrentIndex(newIndex);
		}

		else if(m->currentPlaylistIndex >= m->playlists.count())
		{
			setCurrentIndex(m->currentPlaylistIndex - 1);
		}

		const auto activePlaylist = playlist(activeIndex());
		const auto lastPlaylistId = activePlaylist ? activePlaylist->id() : -1;
		const auto lastTrack = activePlaylist ? currentTrackWithoutDisabled(*activePlaylist) : -1;

		SetSetting(Set::PL_LastTrack, lastTrack);
		SetSetting(Set::PL_LastPlaylist, lastPlaylistId);
	}

	PlaylistPtr Handler::playlist(const int playlistIndex)
	{
		return Util::between(playlistIndex, m->playlists.count())
		       ? m->playlists[playlistIndex]
		       : nullptr;
	}

	PlaylistPtr Handler::playlistById(const int playlistId)
	{
		const auto index = Util::Algorithm::indexOf(m->playlists, [&](const auto playlist) {
			return (playlist->id() == playlistId);
		});

		return playlist(index);
	}

	int Handler::exists(const QString& name) const
	{
		if(name.isEmpty() && Util::between(m->currentPlaylistIndex, m->playlists))
		{
			return m->currentPlaylistIndex;
		}

		return Util::Algorithm::indexOf(m->playlists, [&](const auto& playlist) {
			return (playlist->name().toLower() == name.toLower());
		});
	}

	void Handler::playlistRenamed(const int id, const QString& /*oldName*/, const QString& /*newName*/)
	{
		if(const auto playlist = playlistById(id); playlist)
		{
			emit sigPlaylistNameChanged(playlist->index());
		}
	}

	void Handler::playlistDeleted(const int id)
	{
		if(auto playlist = playlistById(id); playlist)
		{
			playlist->setTemporary(true);
			playlist->setChanged(false);
		}
	}

	void Handler::wwwTrackFinished(const MetaData& track)
	{
		if(GetSetting(Set::Stream_ShowHistory))
		{
			if(auto activePlaylist = playlist(activeIndex()); activePlaylist)
			{
				insertTracks(*activePlaylist, MetaDataList {track}, activePlaylist->findCurrentTrackIndex(),
				             Reason::StreamHistory);
			}
		}
	}

	PlaylistPtr Handler::determineActivePlaylist()
	{
		if(const auto& result = playlist(activeIndex()); isPlaylistReadyForPlayback(result))
		{
			return result;
		}

		if(const auto& result = playlist(currentIndex()); isPlaylistReadyForPlayback(result))
		{
			return result;
		}

		return playlist(Util::Algorithm::indexOf(m->playlists, isPlaylistReadyForPlayback));
	}
}
