#include "category-selection.hpp"
#include "token.hpp"
#include "twitch-helpers.hpp"

#include <name-dialog.hpp>
#include <obs-module-helper.hpp>
#include <obs.hpp>
#include <path-helpers.hpp>
#include <QVBoxLayout>
#include <ui-helpers.hpp>
#include <utility.hpp>

namespace advss {

void TwitchCategory::Load(obs_data_t *obj)
{
	OBSDataAutoRelease data = obs_data_get_obj(obj, "category");
	id = obs_data_get_int(data, "id");
	name = obs_data_get_string(data, "name");
}

void TwitchCategory::Save(obs_data_t *obj) const
{
	OBSDataAutoRelease data = obs_data_create();
	obs_data_set_int(data, "id", id);
	obs_data_set_string(data, "name", name.c_str());
	obs_data_set_obj(obj, "category", data);
}

bool TwitchCategorySelection::_fetchingCategoriesDone = false;
std::map<QString, int> TwitchCategorySelection::_streamingCategories;

void TwitchCategorySelection::PopulateCategorySelection()
{
	auto token = _token.lock();
	if (!token && _streamingCategories.empty()) {
		return;
	}

	if (!_fetchingCategoriesDone && token) {
		_categoryGrabber.Start(token);
		if (_progressDialog->exec() == QDialog::Accepted) {
			_fetchingCategoriesDone = true;
		}
		_categoryGrabber.Stop();
		_categoryGrabber.wait();
	}
	UpdateCategoryList();
}

void TwitchCategorySelection::UpdateCategoryList()
{
	_streamingCategories = _categoryGrabber.GetCategories();
	QString currentSelection = currentText();

	const QSignalBlocker b(this);
	clear();
	for (const auto &[name, id] : _streamingCategories) {
		addItem(name, id);
	}

	setCurrentText(currentSelection);
}

TwitchCategorySelection::TwitchCategorySelection(QWidget *parent)
	: FilterComboBox(
		  parent,
		  obs_module_text("AdvSceneSwitcher.twitchCategories.select")),
	  _progressDialog(new ProgressDialog(this))
{
	_progressDialog->setWindowModality(Qt::WindowModal);
	setSizeAdjustPolicy(QComboBox::AdjustToContents);

	QWidget::connect(this, SIGNAL(currentIndexChanged(int)), this,
			 SLOT(SelectionChanged(int)));
	QWidget::connect(&_categoryGrabber, SIGNAL(CategoryCountUpdated(int)),
			 _progressDialog, SLOT(CategoryCountUpdated(int)));
	QWidget::connect(&_categoryGrabber, SIGNAL(Finished()), this,
			 SLOT(PopulateFinished()));
	QWidget::connect(TwitchCategorySignalManager::Instance(),
			 SIGNAL(RepopulateRequired()), this,
			 SLOT(UpdateCategoryList()));
}

void TwitchCategorySelection::SetToken(const std::weak_ptr<TwitchToken> &token)
{
	_token = token;
	const bool expired = token.expired();
	setDisabled(expired);
	if (expired) {
		setToolTip(obs_module_text(
			"AdvSceneSwitcher.action.twitch.categorySelectionDisabled"));
	} else {
		setToolTip("");
	}
}

void TwitchCategorySelection::PopulateFinished()
{
	_progressDialog->accept();
}

void TwitchCategorySelection::showPopup()
{
	if (!IsPopulated()) {
		PopulateCategorySelection();
	}
	adjustSize();
	updateGeometry();
	FilterComboBox::showPopup();
}

bool TwitchCategorySelection::IsPopulated()
{
	return count() == (int)_categoryGrabber.GetCategories().size() &&
	       _fetchingCategoriesDone;
}

void TwitchCategorySelection::SetCategory(const TwitchCategory &id)
{
	// If the list is populated already try to find id ...
	int index = findData(id.id);
	if (index != -1) {
		setCurrentIndex(index);
		return;
	}

	if (id.id == -1) {
		setCurrentIndex(-1);
		return;
	}

	// ... otherwise just add a dummy entry with the category name
	addItem(QString::fromStdString(id.name), id.id);
	setCurrentIndex(findData(id.id));
}

void TwitchCategorySelection::SelectionChanged(int index)
{
	TwitchCategory category{itemData(index).toInt(),
				currentText().toStdString()};
	emit CategoryChanged(category);
}

std::map<QString, int> CategoryGrabber::_categoryMap = {};
std::mutex CategoryGrabber::_mtx = {};

void CategoryGrabber::Start(const std::shared_ptr<TwitchToken> &token,
			    const std::string search)
{
	_searchString = search;
	_token = token;
	_stop = false;
	start();
}

void CategoryGrabber::Stop()
{
	_stop = true;
}

const std::map<QString, int> &CategoryGrabber::GetCategories()
{
	return _categoryMap;
}

void CategoryGrabber::run()
{
	if (!_token) {
		emit Failed();
		return;
	}

	{
		std::lock_guard<std::mutex> lock(_mtx);
		if (_searchString.empty()) {
			GetAll();
		} else {
			Search(_searchString);
		}
	}

	emit Finished();
}

void CategoryGrabber::Search(const std::string &)
{
	static const std::string uri = "https://api.twitch.tv";
	const std::string path = "/helix/search/categories";

	int startCount = _categoryMap.size();
	std::string cursor;
	httplib::Params params = {{"first", "100"},
				  {"after", cursor},
				  {"query", _searchString}};
	auto response = SendGetRequest(*_token, uri, path, params, true);

	while (response.status == 200 && !_stop) {
		cursor = ParseReply(response.data);
		if (cursor.empty()) {
			break; // End of category list
		}
		params = {{"first", "100"},
			  {"after", cursor},
			  {"query", _searchString}};
		response = SendGetRequest(*_token, uri, path, params, true);
		emit CategoryCountUpdated(_categoryMap.size() - startCount);
	}
}

void CategoryGrabber::GetAll()
{
	static const std::string uri = "https://api.twitch.tv";
	const std::string path = "/helix/games/top";

	// Declare static to "save" progress in case of cancel
	static std::string cursor;

	httplib::Params params = {{"first", "100"}, {"after", cursor}};
	auto response = SendGetRequest(*_token, uri, path, params, true);

	while (response.status == 200 && !_stop) {
		cursor = ParseReply(response.data);
		if (cursor.empty()) {
			break; // End of category list
		}
		params = {{"first", "100"}, {"after", cursor}};
		response = SendGetRequest(*_token, uri, path, params, true);
		emit CategoryCountUpdated(_categoryMap.size());
	}
}

std::string CategoryGrabber::ParseReply(obs_data_t *data) const
{
	OBSDataArrayAutoRelease array = obs_data_get_array(data, "data");
	size_t count = obs_data_array_count(array);
	for (size_t i = 0; i < count; i++) {
		OBSDataAutoRelease arrayObj = obs_data_array_item(array, i);
		int id = std::stoi(obs_data_get_string(arrayObj, "id"));
		QString name = obs_data_get_string(arrayObj, "name");
		_categoryMap.emplace(name, id);
	}
	OBSDataAutoRelease pagination = obs_data_get_obj(data, "pagination");
	return obs_data_get_string(pagination, "cursor");
}

ProgressDialog::ProgressDialog(QWidget *parent, bool showSkip)
	: QDialog(parent),
	  _skipFetchCheckBox(new QCheckBox(obs_module_text(
		  "AdvSceneSwitcher.twitchCategories.fetchSkip"))),
	  _status(new QLabel(obs_module_text(
		  "AdvSceneSwitcher.twitchCategories.fetchStart")))
{
	setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
	auto layout = new QVBoxLayout(this);
	layout->addWidget(_status);
	auto cancelButton = new QPushButton(
		obs_module_text("AdvSceneSwitcher.twitchCategories.fetchStop"),
		this);
	layout->addWidget(_skipFetchCheckBox);
	layout->addWidget(cancelButton);
	setLayout(layout);

	QWidget::connect(_skipFetchCheckBox, &QCheckBox::stateChanged, this,
			 [this](int value) { _skipFetch = value; });
	QWidget::connect(cancelButton, &QPushButton::clicked, this,
			 [this]() { _skipFetch ? accept() : reject(); });

	_skipFetchCheckBox->setVisible(showSkip);
	if (_skipFetch) {
		accept();
	}
}

void ProgressDialog::CategoryCountUpdated(int value)
{
	_status->setText(
		QString(obs_module_text(
				"AdvSceneSwitcher.twitchCategories.fetchStatus"))
			.arg(value));
}

TwitchCategorySearchButton::TwitchCategorySearchButton(QWidget *parent)
	: QToolButton(parent)
{
	const std::string pathPrefix =
		GetDataFilePath("res/images/" + GetThemeTypeName());
	SetButtonIcon(this, (pathPrefix + "Search.svg").c_str());
	setToolTip(obs_module_text(
		"AdvSceneSwitcher.twitchCategories.manualSearch"));
	QWidget::connect(this, SIGNAL(clicked()), this,
			 SLOT(StartManualCategorySearch()));
	QWidget::connect(this, SIGNAL(RequestRepopulate()),
			 TwitchCategorySignalManager::Instance(),
			 SIGNAL(RepopulateRequired()));
}

void TwitchCategorySearchButton::SetToken(
	const std::weak_ptr<TwitchToken> &token)
{
	_token = token;
	const bool expired = token.expired();
	setDisabled(expired);
	if (expired) {
		setToolTip(obs_module_text(
			"AdvSceneSwitcher.action.twitch.categorySelectionDisabled"));
	} else {
		setToolTip(obs_module_text(
			"AdvSceneSwitcher.twitchCategories.manualSearch"));
	}
}

void TwitchCategorySearchButton::StartManualCategorySearch()
{
	std::string category;
	bool accepted = NameDialog::AskForName(
		this,
		obs_module_text("AdvSceneSwitcher.twitchCategories.search"),
		obs_module_text("AdvSceneSwitcher.twitchCategories.name"),
		category);
	if (!accepted) {
		return;
	}

	CategoryGrabber categoryGrabber;
	auto *progressDialog = new ProgressDialog(this, false);

	QWidget::connect(&categoryGrabber, SIGNAL(CategoryCountUpdated(int)),
			 progressDialog, SLOT(CategoryCountUpdated(int)));
	QWidget::connect(&categoryGrabber, &CategoryGrabber::Finished, this,
			 [progressDialog]() { progressDialog->accept(); });
	QWidget::connect(&categoryGrabber, &CategoryGrabber::Failed, this,
			 [progressDialog]() { progressDialog->reject(); });

	auto previousCategoryCount = categoryGrabber.GetCategories().size();

	categoryGrabber.Start(_token.lock(), category);
	progressDialog->exec();
	categoryGrabber.Stop();
	categoryGrabber.wait();

	emit RequestRepopulate();
	progressDialog->deleteLater();

	auto newCategoryCount =
		categoryGrabber.GetCategories().size() - previousCategoryCount;
	if (newCategoryCount == 0) {
		DisplayMessage(
			QString(obs_module_text(
					"AdvSceneSwitcher.twitchCategories.searchFailed"))
				.arg(QString::fromStdString(category)));
	} else {
		DisplayMessage(
			QString(obs_module_text(
					"AdvSceneSwitcher.twitchCategories.searchSuccess"))
				.arg(QString::number(newCategoryCount),
				     QString::fromStdString(category)));
	}
}

TwitchCategoryWidget::TwitchCategoryWidget(QWidget *parent)
	: QWidget(parent),
	  _selection(new TwitchCategorySelection(this)),
	  _manualSearch(new TwitchCategorySearchButton(this))
{
	QWidget::connect(_selection,
			 SIGNAL(CategoryChanged(const TwitchCategory &)), this,
			 SIGNAL(CategoryChanged(const TwitchCategory &)));

	auto layout = new QHBoxLayout();
	layout->setContentsMargins(0, 0, 0, 0);
	layout->addWidget(_selection);
	layout->addWidget(_manualSearch);
	setLayout(layout);
}

void TwitchCategoryWidget::SetCategory(const TwitchCategory &category)
{
	_selection->SetCategory(category);
}

void TwitchCategoryWidget::SetToken(const std::weak_ptr<TwitchToken> &token)
{
	_selection->SetToken(token);
	_manualSearch->SetToken(token);
}

TwitchCategorySignalManager *TwitchCategorySignalManager::Instance()
{
	static TwitchCategorySignalManager manager;
	return &manager;
}

} // namespace advss
