#include "src/core/updates.hpp"

#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QDateTime>
#include <QCoreApplication>
#include <QMetaObject>
#include <sstream>
#include <queue>
#include "src/polyfill/base-directory.hpp"
#include "src/core/retroarch-sdl-fix.hpp"
#include "src/core/file-controller.hpp"
#include "src/core/numeric-string.hpp"
#include "src/core/filesystem.hpp"
#include "src/core/retroarch.hpp"
#include "src/core/logging.hpp"
#include "src/core/time.hpp"
#include "src/core/zip.hpp"
#include "src/ui/download-dialog.hpp"
#include "src/ui/core-installer.hpp"
#include "src/updaters/retroarch-updater.hpp"
#ifdef __APPLE__
#include "src/polyfill/macos/apple-util.hpp"
#endif

#if QT_VERSION >= QT_VERSION_CHECK( 5, 15, 0 )
	#define ON_NETWORK_ERROR QOverload<QNetworkReply::NetworkError>::of( &QNetworkReply::errorOccurred )
#else
	#define ON_NETWORK_ERROR QOverload<QNetworkReply::NetworkError>::of( &QNetworkReply::error )
#endif

const InstalledVersionsInfo InstalledVersionsInfo::Default {
	/* retroArchVersion */ RetroArchVersion{ Version{ 0, 0, 0 }, false },
	/* parallelVersion */ Version{ 0, 0, 0 },
	/* lastUpdateCheck */ 0
};

static constexpr char P_VERSION[] = "version";
static constexpr char P_LOCK[] = "lock";

template<> void JsonSerializer::serialize<RetroArchVersion>( JsonWriter &jw, const RetroArchVersion &obj ) {
	jw.writeObjectStart();
	jw.writePropertyName( P_VERSION );
	serialize( jw, obj.version );
	jw.writeProperty( P_LOCK, obj.lock );
	jw.writeObjectEnd();
}

template<> RetroArchVersion JsonSerializer::parse<RetroArchVersion>( const Json &json ) {
	Version version = Version{ 0, 0, 0 };
	if( json[P_VERSION].exists() ) {
		version = parse<Version>( json[P_VERSION] );
	}

	return RetroArchVersion {
		version,
		json[P_LOCK].getOrDefault<bool>( false )
	};
}

static constexpr char P_RETROARCH[] = "retroarch";
static constexpr char P_PARALLEL[] = "parallel";
static constexpr char P_LAST_CHECKED[] = "last_checked";

template<> void JsonSerializer::serialize<InstalledVersionsInfo>( JsonWriter &jw, const InstalledVersionsInfo &obj ) {
	jw.writeObjectStart();
	jw.writePropertyName( P_RETROARCH );
	serialize( jw, obj.retroArchVersion );
	jw.writePropertyName( P_PARALLEL );
	serialize( jw, obj.parallelVersion );
	jw.writeProperty( P_LAST_CHECKED, obj.lastUpdateCheck );
	jw.writeObjectEnd();
}

template<> InstalledVersionsInfo JsonSerializer::parse<InstalledVersionsInfo>( const Json &json ) {
	Version parallelVersion;
	try {
		parallelVersion = parse<Version>( json[P_PARALLEL] );
	} catch( ... ) {
		parallelVersion = { 0, 0, 0 };
	}

	RetroArchVersion retroVersion;
	try {
		retroVersion = parse<RetroArchVersion>( json[P_RETROARCH] );
	} catch( ... ) {
		retroVersion = RetroArchVersion{ Version{ 0, 0, 0 }, false };
	}

	return InstalledVersionsInfo {
		retroVersion,
		parallelVersion,
		json[P_LAST_CHECKED].get<int64>()
	};
}

template<> ParallelCoreVersion JsonSerializer::parse<ParallelCoreVersion>( const Json &json ) {
#if defined(__linux__)
	const Json &vjson = json["linux_x64"];
#elif defined(_WIN32)
	#ifdef _WIN64
		const Json &vjson = json["windows_x64"];
	#else
		const Json &vjson = json["windows_x86"];
	#endif
#elif defined(__APPLE__)
	const Json &vjson = AppleUtil::shouldUseArmCore() ? json["macos_arm64"] : json["macos_x64"];
#else
	const Json &vjson = json;
	static_assert( false );
#endif

	return ParallelCoreVersion{
		JsonSerializer::parse<Version>( vjson["version"] ),
		vjson["sha1"].get<string>(),
		vjson["url"].get<string>()
	};
}

static void getAsync(
	QNetworkAccessManager *web,
	const string &url,
	const std::function<void(const Json&)> &onSuccess,
	const std::function<void(void)> &onFailure
) {
	QNetworkReply *response = web->get( QNetworkRequest( QUrl( url.c_str() ) ) );
	logDebug( "GET "s + url );

	QObject::connect( response, ON_NETWORK_ERROR, web, [=](QNetworkReply::NetworkError error) {
		(void)error;
		response->deleteLater();
		onFailure();
	}, Qt::QueuedConnection );

	QObject::connect( response, &QNetworkReply::finished, web, [=](){
		if( response->error() != QNetworkReply::NoError ) return;
		const string responseBody = response->readAll().toStdString();
		response->deleteLater();

		std::stringstream jsonStream( responseBody );
		Json jsonBody;
		try {
			jsonBody = Json::parse( jsonStream );
		} catch( ... ) {
			onFailure();
			return;
		}

		onSuccess( jsonBody );
	}, Qt::QueuedConnection );
}

static inline bool checkSchedule(
	const InstalledVersionsInfo &installedVersions
) {
	const QDate now = QDateTime::currentDateTime().date();
	const QDate lastUpdate = QDateTime::fromSecsSinceEpoch( installedVersions.lastUpdateCheck ).date();

	if( now.year() != lastUpdate.year() ) {
		return true;
	}

	switch( FileController::loadAppSettings().coreUpdateInterval ) {
		case CoreUpdateInterval::EveryLaunch:
			return true;
		case CoreUpdateInterval::Daily:
			return now.dayOfYear() != lastUpdate.dayOfYear();
		case CoreUpdateInterval::Weekly:
			return now.weekNumber() != lastUpdate.weekNumber();
		case CoreUpdateInterval::Monthly:
			return now.month() != lastUpdate.month();
		default:
			return false;
	}
}

void RetroUpdater::checkForUpdates(
	bool waitForCoreUpdates,
	bool forceUpdate
) {
	InstalledVersionsInfo installedVersions = FileController::loadInstalledVersions();

	if( forceUpdate || checkSchedule( installedVersions ) ) {
#ifndef FLATPAK_VERSION
		if( !installedVersions.retroArchVersion.lock ) {
			RetroArchUpdater::update();
		}
#endif

		if( waitForCoreUpdates ) {
			CoreInstaller::checkForUpdatesSync( installedVersions );
			FileController::saveInstalledVersions( installedVersions );
		} else {
			CoreInstaller::checkForUpdatesAsync();
		}
	}
}

void ParallelCoreBuilds::getLatest(
	const std::function<void(const ParallelCoreVersion&)> &onSuccess,
	const std::function<void(void)> &onFailure
) {
	QNetworkAccessManager *web = new QNetworkAccessManager();
	getAsync(
		web,
		"https://parallel-launcher.ca/latest/paralleln64/version.json",
		[=]( const Json &body ) {
			try {
				const ParallelCoreVersion version = JsonSerializer::parse<ParallelCoreVersion>( body );
				onSuccess( version );
				web->deleteLater();
			} catch( ... ) {
				web->deleteLater();
				onFailure();
			}
		},
		[=]() {
			web->deleteLater();
			onFailure();
		}
	);
}
