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

/*
 * CoverFetchThread.cpp
 *
 *  Created on: Jun 28, 2011
 *      Author: Michael Lugmair (Lucio Carreras)
 */

#include "CoverFetchThread.h"
#include "CoverLocation.h"
#include "CoverFetchManager.h"
#include "Fetcher/CoverFetcher.h"
#include "Fetcher/CoverFetcherUrl.h"

#include "Utils/Algorithm.h"
#include "Utils/Logger/Logger.h"
#include "Utils/WebAccess/WebClientImpl.h"
#include "Utils/globals.h"

#include <QMap>
#include <QPainter>
#include <QPixmap>
#include <QMap>
#include <QStringList>
#include <QtSvg/QSvgRenderer>

#include <atomic>
#include <mutex>

using namespace Cover;
using Fetcher::Url;
using RequestMap = QMap<WebClient*, QMetaObject::Connection>;

namespace
{
	constexpr const auto Timeout = 5000U;

	bool isValidPixmap(const QPixmap& pixmap)
	{
		constexpr const auto MinimumPixmapSize = 50;
		return !pixmap.isNull() &&
		       (pixmap.width() >= MinimumPixmapSize) &&
		       (pixmap.height() >= MinimumPixmapSize);
	}

	QPixmap extractPixmap(const WebClient* webAccess)
	{
		QPixmap pixmap;
		if(webAccess->url().endsWith("svg", Qt::CaseInsensitive))
		{
			constexpr const auto PixmapSize = 1000;
			pixmap = QPixmap(PixmapSize, PixmapSize);
			pixmap.fill(Qt::transparent);
			auto painter = QPainter(&pixmap);
			auto renderer = QSvgRenderer(webAccess->data());
			renderer.render(&painter);
		}

		return pixmap.isNull()
		       ? QPixmap::fromImage(QImage::fromData(webAccess->data()))
		       : pixmap;
	}
}

struct WebCoverFetcher::Private
{
	RequestMap runningRequests;
	Fetcher::CoverFetcherPtr coverFetcher{nullptr};
	Fetcher::Manager* coverFetchManager{Fetcher::Manager::instance()};

	QList<QPixmap> pixmaps;

	QStringList imageAddresses;
	QStringList foundUrls;
	QList<Url> searchUrls;
	const int requestedCovers;
	std::atomic<bool> stopped{false};
	std::mutex mutex;

	Private(const Location& coverLocation, const int requestedCovers) :
		requestedCovers(requestedCovers)
	{
		Util::Algorithm::copyIf(coverLocation.searchUrls(), this->searchUrls, [&](const auto& url) {
			return (this->coverFetchManager->isActive(url.identifier()) && (!url.url().isEmpty()));
		});

		Util::Algorithm::remove_duplicates(this->searchUrls);
	}
};

WebCoverFetcher::WebCoverFetcher(QObject* parent, const Location& coverLocation, const int requestedCovers) :
	QObject(parent),
	m{Pimpl::make<Private>(coverLocation, requestedCovers)} {}

WebCoverFetcher::~WebCoverFetcher()
{
	cleanup();
}

void WebCoverFetcher::startRequest(const QString& url, std::function<void()>&& callback)
{
	auto* webClient = new WebClientImpl(this);
	webClient->setMode(WebClient::Mode::AsBrowser);

	const auto connection = connect(webClient, &WebClient::sigFinished, this, callback);

	{
		std::lock_guard lock(m->mutex);
		m->runningRequests.insert(webClient, connection);
	}

	webClient->run(url, Timeout);
}

bool WebCoverFetcher::start()
{
	return startNextRequest();
}

void WebCoverFetcher::stop()
{
	spLog(Log::Verbose, this) << "stopped. " << reinterpret_cast<uint64_t>(this);
	if(!m->stopped)
	{
		m->stopped = true;
		m->searchUrls.clear();
		m->imageAddresses.clear();

		std::lock_guard lock(m->mutex);

		for(auto* webRequest: m->runningRequests.keys())
		{
			webRequest->stop();
			webRequest->deleteLater();
		}

		m->runningRequests.clear();

		emit sigFinished();
	}
}

void WebCoverFetcher::cleanup()
{
	{
		std::lock_guard lock(m->mutex);

		for(auto it = m->runningRequests.begin(); it != m->runningRequests.end(); ++it)
		{
			const auto* request = it.key();
			const auto connection = it.value();

			request->disconnect(connection);
		}
	}

	stop();
}

bool WebCoverFetcher::startNextRequest()
{
	if(m->requestedCovers == m->pixmaps.size())
	{
		spLog(Log::Debug, this) << "Enough covers fetched: " << m->requestedCovers;
		stop();
		return true;
	}

	if(m->imageAddresses.isEmpty())
	{
		if(const auto canStart = processNextSearchUrl(); canStart)
		{
			return true;
		}
	}

	return processNextImageUrl();
}

bool WebCoverFetcher::processNextSearchUrl()
{
	if(m->searchUrls.isEmpty())
	{
		spLog(Log::Verbose, this) << "No more search urls";
		return false;
	}

	const auto url = m->searchUrls.takeFirst();
	spLog(Log::Verbose, this) << QStringLiteral("Process next search url %1: (%2 left)")
	                           .arg(url.url())
	                           .arg(m->searchUrls.size());

	m->coverFetcher = m->coverFetchManager->coverfetcher(url);
	if(!m->coverFetcher)
	{
		return false;
	}

	spLog(Log::Verbose, this) << "Next coverfetcher: " << m->coverFetcher->identifier();

	startRequest(url.url(), [this, directFetcher = m->coverFetcher->canFetchCoverDirectly()] {
		if(directFetcher)
		{
			imageFetched();
		}
		else
		{
			contentFetched();
		}
	});

	return true;
}

bool WebCoverFetcher::processNextImageUrl()
{
	if(m->imageAddresses.isEmpty())
	{
		spLog(Log::Verbose, this) << "No more image urls to process";
		return false;
	}

	const auto url = m->imageAddresses.takeFirst();
	spLog(Log::Verbose, this) << QStringLiteral("Process next image url %1: (%2 left)")
	                           .arg(url)
	                           .arg(m->imageAddresses.size());

	startRequest(url, [this] { imageFetched(); });

	return true;
}

void WebCoverFetcher::contentFetched()
{
	auto* webClient = dynamic_cast<WebClient*>(sender());

	if(const auto hasData = (webClient->status() == WebClient::Status::GotData); !m->stopped && hasData)
	{
		spLog(Log::Verbose, this) << "Content fetched. Adding image address";

		const auto websiteData = webClient->data();
		m->imageAddresses = m->coverFetcher->parseAddresses(websiteData);
	}
	else
	{
		spLog(Log::Verbose, this) << "Content fetched, but not data or stopped: Stopeed?" << m->stopped.load()
			<< ", got data? " << hasData;
	}

	destroyRequest(webClient);
	startNextRequest();
}

void WebCoverFetcher::imageFetched()
{
	auto* webClient = dynamic_cast<WebClient*>(sender());
	if(!m->stopped)
	{
		if(webClient->status() == WebClient::Status::GotData)
		{
			if(const auto pixmap = extractPixmap(webClient); isValidPixmap(pixmap))
			{
				m->pixmaps << pixmap;
				m->foundUrls << webClient->url();

				emit sigCoverFound(m->pixmaps.count() - 1);
			}
		}
	}

	destroyRequest(webClient);
	startNextRequest();
}

void WebCoverFetcher::destroyRequest(WebClient* webClient)
{
	std::lock_guard lock(m->mutex);
	if(m->runningRequests.contains(webClient))
	{
		disconnect(m->runningRequests[webClient]);
		m->runningRequests.remove(webClient);
	}

	webClient->deleteLater();
}

QString WebCoverFetcher::url(const int idx) const
{
	return Util::between(idx, m->foundUrls) ? m->foundUrls[idx] : QString();
}

QPixmap WebCoverFetcher::pixmap(const int idx) const
{
	return Util::between(idx, m->pixmaps) ? m->pixmaps[idx] : QPixmap();
}
