/* PlaybackPipeline.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 "Pipeline.h"

#include "Components/Engine/Callbacks.h"
#include "Components/Engine/EngineUtils.h"
#include "PipelineExtensions/Broadcasting.h"
#include "PipelineExtensions/Changeable.h"
#include "PipelineExtensions/Crossfader.h"
#include "PipelineExtensions/DelayedPlayback.h"
#include "PipelineExtensions/Equalizer.h"
#include "PipelineExtensions/PipelineInterfaces.h"
#include "PipelineExtensions/Pitcher.h"
#include "PipelineExtensions/PositionAccessor.h"
#include "PipelineExtensions/Probing.h"
#include "PipelineExtensions/VisualizerBin.h"
#include "StreamRecorder/StreamRecorderBin.h"
#include "Utils/FileUtils.h"
#include "Utils/Logger/Logger.h"
#include "Utils/Settings/SettingNotifier.h"
#include "Utils/Settings/Settings.h"
#include "Utils/Utils.h"
#include "Utils/globals.h"

#include <QTimer>

#include <gst/app/gstappsink.h>

#include <algorithm>
#include <cmath>
#include <utility>

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma ide diagnostic ignored "cppcoreguidelines-pro-type-cstyle-cast"
#pragma ide diagnostic ignored "bugprone-casting-through-void"

namespace Engine
{
	using Elements = QList<GstElement*>;

	class PipelineImpl :
		public Pipeline,
		public PipelineExtensions::Broadcastable,
		public PipelineExtensions::PlaystateController
	{
		Q_OBJECT
		public:
			PipelineImpl(QString name, QObject* parent) :
				Pipeline(parent),
				m_name {std::move(name)} {}

			~PipelineImpl() override
			{
				if(m_pipeline)
				{
					Utils::setState(m_pipeline, GST_STATE_NULL);
					gst_object_unref(GST_OBJECT(m_pipeline));
					m_pipeline = nullptr;
				}
			}

			bool init(Engine* engine) override
			{
				if(m_pipeline)
				{
					return true;
				}

				if(!createElements() || !initCommonPipeline() || !initPlaybackBin())
				{
					return false;
				}

				configureElements();
				initCallbacks(engine);

				SetSetting(SetNoDB::MP3enc_found, Utils::isLameAvailable());
				SetSetting(SetNoDB::Pitch_found, Utils::isPitchAvailable());

				ListenSetting(Set::Engine_Vol, PipelineImpl::volumeChanged);
				ListenSetting(Set::Engine_Mute, PipelineImpl::muteChanged);
				ListenSettingNoCall(Set::Engine_Sink, PipelineImpl::sinkChanged);

				return true;
			}

			void initCallbacks(Engine* engine)
			{
				auto* bus = gst_pipeline_get_bus(GST_PIPELINE(m_pipeline));
				gst_bus_add_watch(bus, Callbacks::busStateChanged, engine);

				m_progressTimer = new QTimer(this);
				m_progressTimer->setTimerType(Qt::PreciseTimer);
				m_progressTimer->setInterval(Utils::getUpdateInterval()); // NOLINT(bugprone-narrowing-conversions)
				connect(m_progressTimer, &QTimer::timeout, this, [this]() {
					if(Utils::getState(m_pipeline) != GST_STATE_NULL)
					{
						Callbacks::positionChanged(this);
					}
				});

				m_progressTimer->start();
			}

			bool prepare(const QString& uri, const QString& userAgent) override
			{
				stop();

				m_trackContext.userAgent = userAgent;

				Utils::setValue(G_OBJECT(m_fadingVolume), "volume", 1.0);
				volumeChanged();
				muteChanged();

				Utils::setInt64Value(G_OBJECT(m_source), "buffer-duration", GetSetting(Set::Engine_BufferSizeMS));
				Utils::setValues(G_OBJECT(m_source),
				                 "use-buffering", Util::File::isWWW(uri),
				                 "uri", uri.toUtf8().data());

				Utils::setState(m_pipeline, GST_STATE_PAUSED);

				return true;
			}

			void setVisualizerEnabled(const bool isLevelActive, const bool isSpectrumActive) override
			{
				spLog(Log::Info, this) << "Setting level enabled: " << isLevelActive 
					               << ", settings spectrum enabled: " << isSpectrumActive;
				m_visualizer->setEnabled(isLevelActive, isSpectrumActive);
			}

			[[nodiscard]] bool isLevelVisualizerEnabled() const override { return m_visualizer->isLevelEnabled(); }

			[[nodiscard]] bool
			isSpectrumVisualizerEnabled() const override { return m_visualizer->isSpectrumEnabled(); }

			void setBroadcastingEnabled(const bool b) override { m_broadcaster->setEnabled(b); }

			[[nodiscard]] bool isBroadcastingEnabled() const override { return m_broadcaster->isEnabled(); }

			[[nodiscard]] GstState state() const override { return Utils::getState(m_pipeline); }

			void checkPosition() override
			{
				emit sigPositionChangedMs(std::max<MilliSeconds>(0, m_positionAccessor->positionMs()));

				const auto aboutToFinishThreshold =
					std::max<MilliSeconds>(PipelineExtensions::Crossfader::fadingTimeMs(), 300);

				if(m_positionAccessor->isAboutToFinish(aboutToFinishThreshold))
				{
					const auto timeToGo = m_positionAccessor->durationMs() - m_positionAccessor->positionMs();
					emit sigAboutToFinishMs(timeToGo);
				}
			}

			[[nodiscard]] bool hasElement(GstElement* e) const override
			{
				return Utils::hasElement(GST_BIN(m_pipeline), e);
			}

			void fadeIn() override { m_crossfader->fadeIn(); }

			void fadeOut() override { m_crossfader->fadeOut(); }

			void startDelayedPlayback(const MilliSeconds ms) override { m_delayedInvoker->playIn(ms); }

			void seekRelative(const double percent, const MilliSeconds duration) override
			{
				m_positionAccessor->seekRelative(percent, duration);
			}

			void seekAbsoluteMs(const MilliSeconds duration) override { m_positionAccessor->seekAbsoluteMs(duration); }

			void seekRelativeMs(const MilliSeconds ms) override
			{
				m_positionAccessor->seekAbsoluteMs(m_positionAccessor->positionMs() + ms);
			}

			[[nodiscard]] MilliSeconds duration() const override { return m_positionAccessor->durationMs(); }

			[[nodiscard]] MilliSeconds timeToGo() const override { return m_positionAccessor->timeToGo(); }

			void setEqualizerBand(const int band, const int value) override { m_equalizer->setBand(band, value); }

		public slots: // NOLINT(*-redundant-access-specifiers)
			void play() override
			{
				Utils::setState(m_pipeline, GST_STATE_PLAYING);
			}

			void pause() override
			{
				Utils::setState(m_pipeline, GST_STATE_PAUSED);
			}

			void stop() override
			{
				Utils::setState(m_pipeline, GST_STATE_NULL);

				m_delayedInvoker->abortDelayedPlaying();
				m_crossfader->abortFading();
			}

		protected:
			void prepareForRecording() override // StreamRecordable
			{
				if(!m_streamRecorder)
				{
					m_streamRecorder = PipelineExtensions::StreamRecorderBin::create(m_pipeline, m_tee);
					m_streamRecorder->init();
				}
			}

			void finishRecording() override // StreamRecordable
			{
				m_streamRecorder->setTargetPath({});
			}

			void setRecordingPath(const QString& path) override // StreamRecordable
			{
				m_streamRecorder->setTargetPath(path);
			}

		private:
			bool createElements()
			{
				m_pipeline = gst_pipeline_new(m_name.toStdString().c_str());
				if(!Utils::testAndError(m_pipeline, "Engine: Pipeline could not be created"))
				{
					return false;
				}

				// input
				if(!Utils::createElement(&m_source, "uridecodebin", "src")) { return false; }
				if(!Utils::createElement(&m_audioConvert, "audioconvert")) { return false; }
				if(!Utils::createElement(&m_fadingVolume, "volume", "fadingVolume")) { return false; }
				if(!Utils::createElement(&m_tee, "tee")) { return false; }
				if(!Utils::createElement(&m_playbackQueue, "queue", "playback_queue")) { return false; }
				if(!Utils::createElement(&m_playbackVolume, "volume", "playbackVolume")) { return false; }

				m_playbackSink = Utils::createSink(GetSetting(Set::Engine_Sink));

				m_visualizer = PipelineExtensions::createVisualizerBin(m_pipeline, m_tee);
				m_rawDataReceiver = PipelineExtensions::createRawDataReceiver([this](const auto& data) {
					emit sigDataAvailable(data);
				});

				// Remember: Fake gstreamer plugins
				m_broadcaster = PipelineExtensions::createBroadcaster(m_rawDataReceiver, m_pipeline, m_tee);
				m_pitcher = PipelineExtensions::createPitcher(m_audioConvert, m_fadingVolume);
				m_crossfader = PipelineExtensions::createCrossfader(this, m_fadingVolume);
				m_delayedInvoker = PipelineExtensions::createDelayedPlaybackInvoker(this);
				m_positionAccessor = PipelineExtensions::createPositionAccessor(m_pipeline, m_tee);
				m_equalizer = PipelineExtensions::createEqualizer();

				return (m_playbackSink != nullptr);
			}

			bool initCommonPipeline()
			{
				auto* equalizer = m_equalizer->equalizerElement();
				auto elements = (equalizer != nullptr)
				                ? Elements {m_source, m_audioConvert, m_fadingVolume, equalizer, m_tee}
				                : Elements {m_source, m_audioConvert, m_fadingVolume, m_tee};

				const auto added = Utils::addElements(GST_BIN(m_pipeline), elements);
				if(!added)
				{
					return Utils::testAndErrorBool(added, "Engine: Cannot add common elements");
				}

				const auto linked = Utils::linkElements(elements.mid(1));
				if(linked)
				{
					// set by gui, initialized directly in pipeline
					ListenSettingNoCall(Set::Engine_Pitch, PipelineImpl::speedChanged);
					ListenSettingNoCall(Set::Engine_Speed, PipelineImpl::speedChanged);
					ListenSettingNoCall(Set::Engine_PreservePitch, PipelineImpl::speedChanged);
					ListenSetting(Set::Engine_SpeedActive, PipelineImpl::speedActiveChanged);
				}

				return Utils::testAndErrorBool(linked, "Engine: Cannot create common pipeline");
			}

			bool initPlaybackBin()
			{
				m_playbackBin = gst_bin_new("Playback_bin");

				const auto playbackElements = Elements {{m_playbackQueue, m_playbackVolume, m_playbackSink}};

				const auto playbackElementsCreated = Utils::addElements(GST_BIN(m_playbackBin), playbackElements) &&
				                                     Utils::linkElements(playbackElements);
				if(!Utils::testAndErrorBool(playbackElementsCreated, "Connot create playback elements"))
				{
					return false;
				}

				const auto teeConnected = Utils::addElements(GST_BIN(m_pipeline), {m_playbackBin}) &&
				                          Utils::createGhostPad(GST_BIN(m_playbackBin), m_playbackQueue) &&
				                          Utils::connectTee(m_tee, m_playbackBin, "Equalizer");
				return Utils::testAndErrorBool(teeConnected, "Cannot connect playbackBin to tee");
			}

			void configureElements()
			{
				Utils::setValues(G_OBJECT(m_tee), "silent", true, "allow-not-linked", true);
				Utils::setValues(G_OBJECT(m_fadingVolume), "volume", 1.0);

				Utils::configureQueue(m_playbackQueue);
				Utils::setState(m_pipeline, GST_STATE_NULL);

				g_signal_connect (m_source, "pad-added", G_CALLBACK(Callbacks::decodebinReady), m_audioConvert);
				g_signal_connect (m_source, "source-setup", G_CALLBACK(Callbacks::sourceReady), &m_trackContext);
			}

			void setVolume(const double volume)
			{
				Utils::setValue(G_OBJECT(m_playbackVolume), "volume", volume);
			}

		private slots: // NOLINT(*-redundant-access-specifiers)
			void volumeChanged()
			{
				const auto volume = GetSetting(Set::Engine_Vol) / 100.0;
				setVolume(volume);
			}

			void muteChanged()
			{
				const auto muted = GetSetting(Set::Engine_Mute);
				Utils::setValue(G_OBJECT(m_playbackVolume), "mute", muted);
			}

			void speedActiveChanged()
			{
				const auto isActive = GetSetting(Set::Engine_SpeedActive);
				m_pitcher->setPitchActivated(isActive);
				if(isActive)
				{
					speedChanged();
				}

				if(Utils::getState(m_pipeline) == GST_STATE_PLAYING)
				{
					const auto positionMs = std::max<MilliSeconds>(m_positionAccessor->positionMs(), 0);
					m_positionAccessor->seekNearestMs(positionMs);
				}

				checkPosition();
			}

			void speedChanged()
			{
				if(GetSetting(Set::Engine_SpeedActive))
				{
					m_pitcher->setSpeed(
						GetSetting(Set::Engine_Speed),
						GetSetting(Set::Engine_Pitch) / 440.0, // NOLINT(readability-magic-numbers)
						GetSetting(Set::Engine_PreservePitch));
				}
			}

			void sinkChanged()
			{
				auto* newSink = Utils::createSink(GetSetting(Set::Engine_Sink));
				if(newSink)
				{
					PipelineExtensions::Changeable::replaceSink(m_playbackSink, newSink, m_playbackVolume,
					                                            m_playbackBin);
					m_playbackSink = newSink;
				}
			}

		private: // NOLINT(*-redundant-access-specifiers)
			QString m_name;

			GstElement* m_pipeline = nullptr;
			GstElement* m_source = nullptr;
			GstElement* m_audioConvert = nullptr;
			GstElement* m_tee = nullptr;

			GstElement* m_playbackBin = nullptr;
			GstElement* m_playbackQueue = nullptr;
			GstElement* m_playbackVolume = nullptr;
			GstElement* m_fadingVolume = nullptr;
			GstElement* m_playbackSink = nullptr;

			PipelineExtensions::StreamRecorderBin* m_streamRecorder = nullptr;
			std::shared_ptr<PipelineExtensions::Broadcaster> m_broadcaster = nullptr;
			std::shared_ptr<PipelineExtensions::RawDataReceiver> m_rawDataReceiver = nullptr;
			std::shared_ptr<PipelineExtensions::VisualizerBin> m_visualizer = nullptr;
			std::shared_ptr<PipelineExtensions::Pitcher> m_pitcher = nullptr;
			std::shared_ptr<PipelineExtensions::Crossfader> m_crossfader = nullptr;
			std::shared_ptr<PipelineExtensions::DelayedPlaybackInvoker> m_delayedInvoker = nullptr;
			std::shared_ptr<PipelineExtensions::PositionAccessor> m_positionAccessor = nullptr;
			std::shared_ptr<PipelineExtensions::Equalizer> m_equalizer = nullptr;

			QTimer* m_progressTimer = nullptr;
			Callbacks::TrackContext m_trackContext;
	};

	Pipeline::Pipeline(QObject* parent) :
		QObject(parent) {}

	Pipeline::~Pipeline() = default;

	std::shared_ptr<Pipeline> Pipeline::create(const QString& name, QObject* parent)
	{
		return std::make_shared<PipelineImpl>(name, parent);
	}
}

#include "Pipeline.moc"

#pragma clang diagnostic pop
