/* GUI_LevelPainter.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_LevelPainter.h"
#include "Gui/Plugins/ui_GUI_LevelPainter.h"

#include "Interfaces/AudioDataProvider.h"

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

#include <QPainter>
#include <QBrush>

#include <cassert>
#include <cstring>
#include <array>
#include <vector>
#include <atomic>

namespace
{
	constexpr const auto LookupTableSize = 40U;
	constexpr const auto Channels = 2U;
	using LookupTable = std::array<float, LookupTableSize>;

	using Step = uint_fast8_t;
	using ChannelArray = std::array<float, Channels>;
	using ChannelSteps = std::vector<Step>;
	using StepArray = std::array<ChannelSteps, Channels>;

	LookupTable initLookupTable()
	{ // we want an interval [1.0 ... 0]
		auto result = LookupTable {};

		for(auto i = 0U; i < LookupTableSize; i++)
		{
			result[i] = -(static_cast<float>(i) / (LookupTableSize - 1U)) + 1.0F;
		}

		return result;
	}

	[[nodiscard]] float scale(const LookupTable& lookupTable, const float value)
	{
		assert(value <= 0);
		const auto iVal = static_cast<uint32_t>(-value);

		const auto index = std::clamp(iVal, 0U, LookupTableSize - 1U);
		return lookupTable[index];
	}

	void resizeStepArray(StepArray& steps, const int rects)
	{
		for(auto& channel: steps)
		{
			channel.resize(rects);
			std::fill(channel.begin(), channel.end(), 0);
		}
	}
}

struct GUI_LevelPainter::Private
{
	ChannelArray level {0, 0};
	StepArray steps;
	LookupTable lookupTable {0};
	LevelDataProvider* dataProvider;
	bool isFadeoutCompleted {true};

	std::atomic_flag lock = ATOMIC_FLAG_INIT;

	explicit Private(LevelDataProvider* dataProvider) :
		dataProvider(dataProvider) {}
};

GUI_LevelPainter::GUI_LevelPainter(LevelDataProvider* dataProvider, PlayManager* playManager, QWidget* parent) :
	VisualPlugin(playManager, parent),
	Engine::LevelDataReceiver(),
	m {Pimpl::make<Private>(dataProvider)} {}

GUI_LevelPainter::~GUI_LevelPainter()
{
	m->dataProvider->unregisterLevelReceiver(this);

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

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

void GUI_LevelPainter::finalizeInitialization()
{
	m->lookupTable = initLookupTable();

	resizeStepArray(m->steps, currentStyle().rectCount);

	constexpr const auto InitialValue = -100.0F;
	m->level[0] = scale(m->lookupTable, InitialValue);
	m->level[1] = scale(m->lookupTable, InitialValue);

	PlayerPlugin::Base::finalizeInitialization();

	m->dataProvider->registerLevelReceiver(this);

	reload();
}

QString GUI_LevelPainter::name() const { return "Level"; }

QString GUI_LevelPainter::displayName() const { return tr("Level"); }

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

void GUI_LevelPainter::retranslate()
{
	ui->retranslateUi(this);
}

void GUI_LevelPainter::setLevel(float left, float right)
{
	if(!isUiInitialized() || !isProcessing() || m->lock.test_and_set())
	{
		return;
	}

	m->level[0] = scale(m->lookupTable, left);
	m->level[1] = scale(m->lookupTable, right);

	m->lock.clear();
	update();
}

void GUI_LevelPainter::paint()
{
	auto painter = QPainter(this);
	const auto& style = currentStyle();

	constexpr const auto InitialY = 10;
	const auto yOffset = std::array<int, Channels> {InitialY, InitialY + style.rectHeight + style.verSpacing};
	const auto initialX = (style.rectWidth + style.horSpacing);

	auto zeroCount = 0U;

	for(auto c = 0U; c < Channels; c++)
	{
		// NOLINTNEXTLINE(*-narrowing-conversions)
		const auto coloredRects = static_cast<int>(style.rectCount * m->level[c]);

		auto rect = QRect(0, yOffset[c], style.rectWidth, style.rectHeight);
		for(auto r = 0; r < style.rectCount; r++)
		{
			const auto colorHash = (r < coloredRects) ? -1 : m->steps[c][r];
			const auto color = style.style[r][colorHash];
			painter.fillRect(rect, color);

			if(r < coloredRects)
			{
				m->steps[c][r] = static_cast<Step>(style.fadingSteps - 1);
			}

			else
			{
				// Counts down to 0 and stays there.
				m->steps[c][r] = (m->steps[c][r] > 0) ? (m->steps[c][r] - 1) : 0;
			}

			if(m->steps[c][r] == 0)
			{
				zeroCount++;
			}

			rect.translate(initialX, 0);
		}

		m->isFadeoutCompleted = (zeroCount == (Channels * style.rectCount));
	}
}

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

void GUI_LevelPainter::doFadeoutStep()
{
	Util::Algorithm::transform(m->level, [](const auto& val) {
		return val - 0.1F; // NOLINT(*-magic-numbers)
	});
}

void GUI_LevelPainter::reload()
{
	const auto newHeight = (currentStyle().rectHeight * 2) + (currentStyle().verSpacing + 12);

	setMinimumHeight(newHeight);
	setMaximumHeight(newHeight);

	if(isVisible())
	{
		emit sigReload(this);
	}
}

void GUI_LevelPainter::notifyDataProvider(const bool isActive)
{
	m->dataProvider->levelActiveChanged(isActive);
}

QWidget* GUI_LevelPainter::widget() { return this; }

bool GUI_LevelPainter::hasSmallButtons() const { return true; }

int GUI_LevelPainter::loadStyleIndex() const { return GetSetting(Set::Level_Style); }

void GUI_LevelPainter::saveStyleIndex(const int index) const { SetSetting(Set::Level_Style, index); }

bool GUI_LevelPainter::applyStyle(const ColorStyle& colorStyle)
{
	resizeStepArray(m->steps, colorStyle.rectCount);
	update();

	return true;
}

ColorStyle GUI_LevelPainter::extractStyle(const RawColorStyle& rawColorStyle) const
{
	return Util::createLevelStyle(rawColorStyle, width());
}
