/* GUI_Spectrum.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 "GUI_Spectrum.h"
#include "Gui/Plugins/ui_GUI_Spectrum.h"

#include "Interfaces/AudioDataProvider.h"

#include "Utils/Algorithm.h"
#include "Utils/Settings/Settings.h"
#include "Utils/globals.h"

#include <QPainter>
#include <QList>
#include <QTimer>

#include <cstring>
#include <cmath>
#include <algorithm>
#include <mutex>

namespace
{
	using Step = int8_t;
	using BinSteps = std::vector<Step>;
	using StepArray = std::vector<BinSteps>;

	constexpr const auto MinBinCount = 50U;

	std::vector<float> createLookupTable(const unsigned int bins)
	{
		auto result = std::vector<float> {};
		result.reserve(bins);

		// make bass value smaller than high frequencies
		// log_lu[0] = 0.0.01666667
		// log_lu[50] = 0.37930765
		for(unsigned int i = 0; i < bins; i++)
		{
			constexpr const auto logBase = 10.0F;
			const auto exponent = (i / 140.0F) + 1.0F;
			const auto value = std::pow(logBase, exponent);
			constexpr const auto scaleFactor = (8.0F * 75.0F);
			result.push_back(value / scaleFactor);
		}

		return result;
	}

	int getColoredItemsForBar(const float percent, const int widgetHeight, const int itemHeight, const int spacing)
	{
		const auto coloredHeight = percent * widgetHeight; // NOLINT(*-narrowing-conversions)

		// NOLINTNEXTLINE(*-narrowing-conversions)
		return std::max<int>(0, static_cast<int>(coloredHeight / (itemHeight + spacing) - 1.0F));
	}

	int getItemHeightByCount(const int rectCount, const int widgetHeight, const int spacing)
	{
		return static_cast<int>((widgetHeight / rectCount) - spacing);
	}

	struct FadingInfo
	{
		QColor color;
		Step brightness;
	};

	FadingInfo fadeRectangle(const ColorStyle& colorStyle, const int itemIndex, const bool isFullBrightness,
	                         const Step currentBrightness, const Step maxBrightness)
	{
		const auto colorHash = isFullBrightness ? -1 : currentBrightness;
		const auto newBrightness = isFullBrightness
		                           ? maxBrightness
		                           : std::max<Step>(0, currentBrightness - 1); // NOLINT(*-narrowing-conversions)

		return {colorStyle.style[itemIndex].value(colorHash), newBrightness};
	}

	int getBinWidth(const int widgetWidth, const int binCount, const int horOffset, const int spacing)
	{
		const auto spacings = (binCount - 1) * spacing;
		const auto coloredSpace = widgetWidth - spacings - horOffset;
		constexpr const auto RoundingTolerance = 10;
		return (coloredSpace + RoundingTolerance) / binCount;
	}
}

struct GUI_Spectrum::Private
{
	std::atomic_flag locked = ATOMIC_FLAG_INIT;

	std::vector<float> spec;
	StepArray steps;
	SpectrumDataProvider* dataProvider;
	std::vector<float> logarithmLookupTable;
	bool isFadeoutCompleted {true};

	explicit Private(SpectrumDataProvider* dataProvider) :
		dataProvider(dataProvider) {}

	void setSpectrum(const std::vector<float>& spectrum)
	{
		// s: [-75, 0]
		// s[i] + 75: [0, 75]
		// scaling factor of ( / 75.0) is in log_lu

		spec.clear();
		spec.reserve(spectrum.size());
		isFadeoutCompleted = true;

		for(size_t i = 0; i < spectrum.size(); i++)
		{
			const auto f = (spectrum[i] + 75.0F) * logarithmLookupTable[i];
			spec.push_back(f);
		}
	}

	void resizeSteps(unsigned int binCount, const int rects)
	{
		binCount = std::max(MinBinCount, binCount);
		if(binCount != steps.size())
		{
			steps.resize(binCount);
		}

		for(auto& step: steps)
		{
			step.resize(rects);
			std::fill(step.begin(), step.end(), 0);
		}
	}
};

GUI_Spectrum::GUI_Spectrum(SpectrumDataProvider* dataProvider, PlayManager* playManager, QWidget* parent) :
	VisualPlugin(playManager, parent),
	Engine::SpectrumDataReceiver(),
	m {Pimpl::make<Private>(dataProvider)} {}

GUI_Spectrum::~GUI_Spectrum()
{
	m->dataProvider->unregisterSpectrumReceiver(this);

	if(ui)
	{
		delete ui;
		ui = nullptr;
	}
}

void GUI_Spectrum::initUi()
{
	VisualPlugin::initUi();
	setupParent(this, &ui);
}

void GUI_Spectrum::finalizeInitialization()
{
	const auto bins = static_cast<unsigned int>(GetSetting(Set::Engine_SpectrumBins));

	m->logarithmLookupTable = createLookupTable(bins);

	constexpr const auto InitialValue = -100.0F;
	m->spec.resize(bins, InitialValue);

	PlayerPlugin::Base::finalizeInitialization();

	m->dataProvider->registerSpectrumReceiver(this);
}

QString GUI_Spectrum::name() const { return "Spectrum"; }

QString GUI_Spectrum::displayName() const { return tr("Spectrum"); }

bool GUI_Spectrum::isActive() const { return isProcessing(); }

void GUI_Spectrum::retranslate() {}

void GUI_Spectrum::setSpectrum(const std::vector<float>& spectrum)
{
	if(isUiInitialized() && isProcessing())
	{
		m->setSpectrum(spectrum);
		update();
	}
}

bool GUI_Spectrum::isFadeoutCompleted() const { return m->isFadeoutCompleted; }

void GUI_Spectrum::doFadeoutStep()
{
	Util::Algorithm::transform(m->spec, [](const auto v) {
		return v - 0.1F; // NOLINT(*-magic-numbers)
	});
}

void GUI_Spectrum::notifyDataProvider(const bool isActive)
{
	m->dataProvider->spectrumActiveChanged(isActive);

}

void GUI_Spectrum::paint()
{
	constexpr const auto FirstBinOfInterest = 0;
	constexpr const auto BinsOfInterest = 35;
	constexpr const auto HorizontalOffset = 3;

	const auto& style = currentStyle();
	const auto itemHeight = getItemHeightByCount(style.rectCount, height(), style.verSpacing);
	const auto binWidth = getBinWidth(width(), BinsOfInterest, HorizontalOffset, style.horSpacing);

	auto painter = QPainter(this);
	auto x = HorizontalOffset;
	auto emptyItems = 0;

	for(int bin = FirstBinOfInterest; bin <= (FirstBinOfInterest + BinsOfInterest); bin++)
	{
		const auto coloredRectCount = getColoredItemsForBar(m->spec[bin], height(), itemHeight, style.verSpacing);
		const auto y = height() - itemHeight;

		auto rect = QRect(x, y, binWidth, itemHeight);
		for(int itemIndex = 0; itemIndex < style.rectCount; itemIndex++)
		{
			const auto hasFullBrightness = (itemIndex < coloredRectCount);
			const auto fadeInfo = fadeRectangle(style, itemIndex, hasFullBrightness, m->steps[bin][itemIndex],
			                                    style.fadingSteps);            // NOLINT(*-narrowing-conversions)

			m->steps[bin][itemIndex] = fadeInfo.brightness;
			if(fadeInfo.brightness == 0)
			{
				emptyItems++;
			}

			painter.fillRect(rect, fadeInfo.color);

			rect.translate(0, -(itemHeight + style.verSpacing));
		}

		x += binWidth + style.horSpacing;
	}

	const auto numRectangles = BinsOfInterest * style.rectCount;
	m->isFadeoutCompleted = (emptyItems >= numRectangles);
}

QWidget* GUI_Spectrum::widget()
{
	return ui->lab;
}

bool GUI_Spectrum::hasSmallButtons() const
{
	return false;
}

ColorStyle GUI_Spectrum::extractStyle(const RawColorStyle& rawColorStyle) const
{
	return Util::createSpectrumStyle(rawColorStyle, height());
}

bool GUI_Spectrum::applyStyle(const ColorStyle& colorStyle)
{
	if(m->locked.test_and_set())
	{
		return false;
	}

	const auto bins = static_cast<unsigned int>(GetSetting(Set::Engine_SpectrumBins));
	m->resizeSteps(bins, colorStyle.rectCount);

	update();

	m->locked.clear();
	return true;
}

int GUI_Spectrum::loadStyleIndex() const { return GetSetting(Set::Spectrum_Style); }

void GUI_Spectrum::saveStyleIndex(const int index) const { SetSetting(Set::Spectrum_Style, index); }
