#include "pch.h"
#include <libsecure.h>
#include <sce_random.h>
#include <user_service.h>
#include <web_browser_dialog.h>
#include "PS4.h"

#pragma comment(lib, "libSceHttp_stub_weak.a")
#pragma comment(lib, "libSceRandom_stub_weak.a")
#pragma comment(lib, "libSceSecure.a")
#pragma comment(lib, "libSceSysmodule_stub_weak.a")
#pragma comment(lib, "libSceUserService_stub_weak.a")
#pragma comment(lib, "libSceCommonDialog_stub_weak.a")
#pragma comment(lib, "libSceWebBrowserDialog_stub_weak.a")

using namespace TwitchInGames;

static SceLibSecureErrorType Initialize() {
	// Load Sony's Random module, use it to generate a seed, and initialize
	// Sony's Secure library with it.
	int loadResult= sceSysmoduleLoadModule(SCE_SYSMODULE_RANDOM);
	static char buffer[4160];
	int randomResult= sceRandomGetRandomNumber(buffer, SCE_RANDOM_MAX_SIZE);
	SceLibSecureBlock initialBlock{ buffer, sizeof(buffer) };
	SceLibSecureErrorType const secureResult= sceLibSecureInit(SCE_LIBSECURE_FLAGS_RANDOM_GENERATOR, &initialBlock);
	UNREFERENCED_PARAMETER(loadResult);
	UNREFERENCED_PARAMETER(randomResult);
	return secureResult;
}

void BrowserAuth::rand_s(unsigned* p) {
	// Initialize Sony's Secure library.
	static SceLibSecureErrorType result= Initialize();
	UNREFERENCED_PARAMETER(result);

	// Use it to generate a cryptographically secure random number.  Failing
	// that, use the C library's random number generation function.
	SceLibSecureBlock block{ p, sizeof(*p) };
	if(sceLibSecureRandom(&block) != SCE_LIBSECURE_OK) {
		*p= rand();
	}
}

BrowserAuth::BrowserAuth() {
#ifndef UNIT_TEST
	// Initialize all components necessary for the Web browser dialog.
	auto result= sceSysmoduleLoadModule(SCE_SYSMODULE_WEB_BROWSER_DIALOG);
	DebugWriteLine("sceSysmoduleLoadModule %#x", result);
	ThrowIfFailed(result);
	result= sceCommonDialogInitialize();
	DebugWriteLine("sceCommonDialogInitialize %#x", result);
	if(result != SCE_COMMON_DIALOG_ERROR_ALREADY_SYSTEM_INITIALIZED) {
		ThrowIfFailed(result);
	}
	SceUserServiceInitializeParams userService{};
	userService.priority= SCE_KERNEL_PRIO_FIFO_DEFAULT;
	result= sceUserServiceInitialize(&userService);
	DebugWriteLine("sceUserServiceInitialize %#x", result);
	if(result != SCE_USER_SERVICE_ERROR_ALREADY_INITIALIZED) {
		ThrowIfFailed(result);
	}
	result= sceWebBrowserDialogInitialize();
	DebugWriteLine("sceWebBrowserDialogInitialize %#x", result);
	if(result != SCE_COMMON_DIALOG_ERROR_ALREADY_INITIALIZED) {
		ThrowIfFailed(result);
	}
#endif
}

#ifdef UNIT_TEST
static bool isRunning;
#endif

BrowserAuth::~BrowserAuth() {
#ifdef UNIT_TEST
	isRunning= false;
#endif
}

std::future<tstring> BrowserAuth::Launch(string_t url, tstring const& state, string_t scheme) {
#ifdef UNIT_TEST
	UNREFERENCED_PARAMETER(url);
	UNREFERENCED_PARAMETER(state);
	UNREFERENCED_PARAMETER(scheme);
	if(isRunning) {
		throw TwitchException(FromPlatformError(ERROR_INVALID_OPERATION));
	}
	isRunning= true;
	auto fn= [this] {
		return token;
	};
#else
	// Ensure the Web browser dialog isn't already open.
	auto const result= sceWebBrowserDialogUpdateStatus();
	if(result == SCE_COMMON_DIALOG_STATUS_RUNNING) {
		throw TwitchException(FromPlatformError(ERROR_INVALID_OPERATION));
	}

	// Launch the browser to start the OAuth 2 flow to have the user
	// authenticate.  For client authentication (flow two), there will be a
	// redirect URI with a custom scheme.
	SceWebBrowserDialogParam param;
	sceWebBrowserDialogParamInitialize(&param);
	param.mode= SCE_WEB_BROWSER_DIALOG_MODE_DEFAULT;
	param.url= url;
	SceWebBrowserDialogCallbackInitParam callbackParam{};
	if(scheme != nullptr) {
		callbackParam.size= sizeof(callbackParam);
		callbackParam.type= SCE_WEB_BROWSER_DIALOG_CALLBACK_PARAM_TYPE_URL;
		callbackParam.data= scheme;
		param.callbackInitParam= &callbackParam;
	}
	ThrowIfFailed(sceUserServiceGetInitialUser(&param.userId));
	ThrowIfFailed(sceWebBrowserDialogOpen(&param));
	bool isCustomScheme= scheme != nullptr;
	auto fn= [this, state, isCustomScheme] {
		for(;;) {
			auto result= sceWebBrowserDialogUpdateStatus();
			if(result == SCE_COMMON_DIALOG_STATUS_FINISHED) {
				if(isCustomScheme) {
					SceWebBrowserDialogResult resultParam{};
					SceWebBrowserDialogCallbackResultParam callbackParam{};
					callbackParam.size= sizeof(callbackParam);
					resultParam.callbackResultParam= &callbackParam;
					ThrowIfFailed(sceWebBrowserDialogGetResult(&resultParam));
					auto returnedState= ExtractToken(resultParam.callbackResultParam->data, "state");
					if(returnedState == state) {
						auto s= ExtractToken(resultParam.callbackResultParam->data);
						if(s.empty()) {
							s= ExtractToken(resultParam.callbackResultParam->data, "access_token");
						}
						TokenLock lock(tokenAccessor);
						token= s;
					}
				}
				break;
			}
			sceKernelUsleep(110 * 1000);
		}
		ThrowIfFailed(sceWebBrowserDialogTerminate());
		TokenLock lock(tokenAccessor);
		return token;
	};
#endif
	return std::async(std::launch::deferred, fn);
}

void BrowserAuth::Stop(string_t token_) {
#ifdef UNIT_TEST
	isRunning= false;
	token= token_;
#else
	// Set the token and close the Web browser dialog.
	TokenLock lock(tokenAccessor);
	token= token_;
	lock.unlock();
	sceWebBrowserDialogClose();
#endif
}
