/* CrossFader.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/>.
 */

/* CrossFader.cpp */

#include "Crossfader.h"
#include "Engine/EngineUtils.h"
#include "Utils/Settings/Settings.h"
#include "Utils/Logger/Logger.h"
#include "Utils/typedefs.h"
#include "Utils/Timer.h"

#include <cmath>
#include <thread>

namespace PipelineExtensions
{
	namespace
	{
		constexpr const auto FadingStepTime = std::chrono::milliseconds {50};
		constexpr const auto MinimalVolume = 0.05;
		constexpr const auto MaximalVolume = 1.0;

		struct FadingConfiguration
		{
			int iterations {0};
			double startVolume {MinimalVolume};
			double targetVolume {MaximalVolume};
			double volumeChangePerIteration {0.0};
		};

		FadingConfiguration calcConfiguration(const Crossfader::FadeMode fadeMode)
		{
			auto result = FadingConfiguration {};

			const auto fadingTime = GetSetting(Set::Engine_CrossFaderTime);
			const auto isFadingIn = (fadeMode == Crossfader::FadeMode::FadeIn);

			result.iterations = (fadingTime / static_cast<int>(FadingStepTime.count())) + 1;
			result.startVolume = isFadingIn ? MinimalVolume : MaximalVolume;
			result.targetVolume = isFadingIn ? MaximalVolume : MinimalVolume;
			result.volumeChangePerIteration =
				(result.targetVolume - result.startVolume) / (static_cast<double>(result.iterations) - 1);

			spLog(Log::Verbose, "CrossfaderImpl")
				<< "Volume change per iteration: " << result.volumeChangePerIteration
				<< " every " << FadingStepTime.count() << "ms"
				<< " from Volume " << result.startVolume << " to " << result.targetVolume;

			return result;
		}

		class CrossfaderImpl :
			public PipelineExtensions::Crossfader
		{
			public:
				CrossfaderImpl(PlaystateController* playstateController, GstElement* volume) :
					m_playstateController {playstateController},
					m_volume {volume} {}

				void fadeIn() override
				{
					m_playstateController->play();
					initFader(Crossfader::FadeMode::FadeIn);
				}

				void fadeOut() override
				{
					initFader(Crossfader::FadeMode::FadeOut);
				}

				void abortFading() override
				{
					m_fadeAborted = true;
				}

				[[nodiscard]] FadeMode mode() const override { return m_mode; }

				[[nodiscard]] bool isFading() const override
				{
					return m_timer.isActive() && !m_fadeAborted;
				}

			private:
				bool initFader(const Crossfader::FadeMode mode)
				{
					m_timer.stop();
					m_mode = mode;
					m_fadeAborted = false;

					const auto configuration = calcConfiguration(m_mode);
					m_targetVolume = configuration.targetVolume;
					setVolume(configuration.startVolume);

					constexpr const auto VolumeTolerance = 0.00001;
					const auto isFadeNecessary = (std::fabs(configuration.volumeChangePerIteration) > VolumeTolerance);
					if(isFadeNecessary)
					{
						// NOLINTNEXTLINE(*-narrowing-conversions)
						startTimer(configuration.iterations, configuration.volumeChangePerIteration);
					}

					return isFadeNecessary;
				}

				void startTimer(const int iterations, const double volumeChangePerTimerIteration)
				{
					spLog(Log::Verbose, this) << "Start timer. Interval: " << FadingStepTime.count()
					                          << " iterations: " << iterations;

					m_timer.start(FadingStepTime, iterations, [this, volumeChangePerTimerIteration]() {
						return timerStepTriggered(volumeChangePerTimerIteration);
					});
				}

				bool timerStepTriggered(const double deltaVolume)
				{ // we are inside the thread here, calling Timer::stop() would lead to self-join -> Exception!
					if(!m_fadeAborted)
					{
						const auto isFadingIn = (m_mode == Crossfader::FadeMode::FadeIn);
						const auto current = currentVolume();
						const auto newVolume = current + deltaVolume;
						const auto targetVolumeReached = isFadingIn
						                                 ? (newVolume >= m_targetVolume)
						                                 : (newVolume <= m_targetVolume);

						const auto isNewVolumeValid = (newVolume >= 0) && (newVolume <= 1.0);
						if(isNewVolumeValid && !targetVolumeReached)
						{
							spLog(Log::Verbose, this) << (isFadingIn ? "Fading in: " : "Fading out: ")
							                          << current << " + " << deltaVolume << " = " << newVolume;
							setVolume(newVolume);
							return true;
						}
					}

					finalizeFading();
					return false;
				}

				void finalizeFading()
				{
					const auto isFadingOut = (m_mode == Crossfader::FadeMode::FadeOut);

					spLog(Log::Verbose, this) << (isFadingOut ? "Fading out: " : "Fading in: ")
					                          << "Setting target volume " << m_targetVolume;
					setVolume(m_targetVolume);

					if(isFadingOut)
					{
						spLog(Log::Debug, this) << "Stopping pipeline";
						m_playstateController->stop();
					}
				}

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

				[[nodiscard]] double currentVolume() const
				{
					double volume; // NOLINT(cppcoreguidelines-init-variables)
					g_object_get(m_volume, "volume", &volume, nullptr); // NOLINT(cppcoreguidelines-pro-type-vararg)
					return volume;
				}

				PipelineExtensions::PlaystateController* m_playstateController;
				GstElement* m_volume;
				Utils::Timer m_timer;
				bool m_fadeAborted {false};
				double m_targetVolume {MaximalVolume};

				Crossfader::FadeMode m_mode {Crossfader::FadeMode::NoFading};
		};
	}

	MilliSeconds Crossfader::fadingTimeMs()
	{
		return GetSetting(Set::Engine_CrossFaderActive) ? GetSetting(Set::Engine_CrossFaderTime) : 0;
	}

	Crossfader::~Crossfader() = default;

	std::shared_ptr<Crossfader>
	createCrossfader(PlaystateController* playstateController, GstElement* volume)
	{
		return std::make_shared<CrossfaderImpl>(playstateController, volume);
	}
}