#include <vector>

#include "SDLGLGraphicsContext.h"

#include "Common/GPU/OpenGL/GLFeatures.h"
#include "Common/GPU/thin3d_create.h"

#include "Common/System/NativeApp.h"
#include "Common/System/System.h"
#include "Common/System/Display.h"
#include "Core/Config.h"
#include "Core/ConfigValues.h"
#include "Core/System.h"

#if defined(USING_EGL)
#include "EGL/egl.h"
#endif

class GLRenderManager;

#if defined(USING_EGL)

// TODO: Move these into the class.
static EGLDisplay               g_eglDisplay    = EGL_NO_DISPLAY;
static EGLContext               g_eglContext    = nullptr;
static EGLSurface               g_eglSurface    = nullptr;
static EGLNativeDisplayType     g_Display       = nullptr;
static bool                     g_XDisplayOpen  = false;
static EGLNativeWindowType      g_Window        = (EGLNativeWindowType)nullptr;
static bool useEGLSwap = false;

int CheckEGLErrors(const char *file, int line) {
	EGLenum error;
	const char *errortext = "unknown";
	error = eglGetError();
	switch (error)
	{
		case EGL_SUCCESS: case 0:           return 0;
		case EGL_NOT_INITIALIZED:           errortext = "EGL_NOT_INITIALIZED"; break;
		case EGL_BAD_ACCESS:                errortext = "EGL_BAD_ACCESS"; break;
		case EGL_BAD_ALLOC:                 errortext = "EGL_BAD_ALLOC"; break;
		case EGL_BAD_ATTRIBUTE:             errortext = "EGL_BAD_ATTRIBUTE"; break;
		case EGL_BAD_CONTEXT:               errortext = "EGL_BAD_CONTEXT"; break;
		case EGL_BAD_CONFIG:                errortext = "EGL_BAD_CONFIG"; break;
		case EGL_BAD_CURRENT_SURFACE:       errortext = "EGL_BAD_CURRENT_SURFACE"; break;
		case EGL_BAD_DISPLAY:               errortext = "EGL_BAD_DISPLAY"; break;
		case EGL_BAD_SURFACE:               errortext = "EGL_BAD_SURFACE"; break;
		case EGL_BAD_MATCH:                 errortext = "EGL_BAD_MATCH"; break;
		case EGL_BAD_PARAMETER:             errortext = "EGL_BAD_PARAMETER"; break;
		case EGL_BAD_NATIVE_PIXMAP:         errortext = "EGL_BAD_NATIVE_PIXMAP"; break;
		case EGL_BAD_NATIVE_WINDOW:         errortext = "EGL_BAD_NATIVE_WINDOW"; break;
		default:                            errortext = "unknown"; break;
	}
	fprintf( stderr, "ERROR: EGL Error %s detected in file %s at line %d (0x%X)\n", errortext, file, line, error );
	return 1;
}

#define EGL_ERROR(str, check) { \
		if (check) CheckEGLErrors( __FILE__, __LINE__ ); \
		fprintf(stderr, "EGL ERROR: " str "\n"); \
		return 1; \
	}

static bool EGL_OpenInit() {
	if ((g_eglDisplay = eglGetDisplay(g_Display)) == EGL_NO_DISPLAY) {
		EGL_ERROR("Unable to create EGL display.", true);
		return false;
	}
	if (eglInitialize(g_eglDisplay, NULL, NULL) != EGL_TRUE) {
		EGL_ERROR("Unable to initialize EGL display.", true);
		eglTerminate(g_eglDisplay);
		g_eglDisplay = EGL_NO_DISPLAY;
		return false;
	}

	return true;
}

static int8_t EGL_Open(SDL_Window *window) {
#if defined(USING_FBDEV)
	g_Display = (EGLNativeDisplayType)nullptr;
	g_Window = (EGLNativeWindowType)nullptr;
#elif defined(__APPLE__)
	g_Display = (EGLNativeDisplayType)XOpenDisplay(nullptr);
	g_XDisplayOpen = g_Display != nullptr;
	if (!g_XDisplayOpen)
		EGL_ERROR("Unable to get display!", false);
	g_Window = (EGLNativeWindowType)nullptr;
#else
	// Get the SDL window native handle
	SDL_SysWMinfo sysInfo{};
	SDL_VERSION(&sysInfo.version);
	if (!SDL_GetWindowWMInfo(window, &sysInfo)) {
		fprintf(stderr, "ERROR: Unable to retrieve native window handle\n");
		g_Display = (EGLNativeDisplayType)XOpenDisplay(nullptr);
		g_XDisplayOpen = g_Display != nullptr;
		if (!g_XDisplayOpen)
			EGL_ERROR("Unable to get display!", false);
		g_Window = (EGLNativeWindowType)nullptr;
	} else {
		switch (sysInfo.subsystem) {
		case SDL_SYSWM_X11:
			g_Display = (EGLNativeDisplayType)sysInfo.info.x11.display;
			g_Window = (EGLNativeWindowType)sysInfo.info.x11.window;
			break;
#if defined(SDL_VIDEO_DRIVER_DIRECTFB)
		case SDL_SYSWM_DIRECTFB:
			g_Display = (EGLNativeDisplayType)EGL_DEFAULT_DISPLAY;
			g_Window = (EGLNativeWindowType)sysInfo.info.dfb.surface;
			break;
#endif
#if SDL_VERSION_ATLEAST(2, 0, 2) && defined(SDL_VIDEO_DRIVER_WAYLAND)
		case SDL_SYSWM_WAYLAND:
			g_Display = (EGLNativeDisplayType)sysInfo.info.wl.display;
			g_Window = (EGLNativeWindowType)sysInfo.info.wl.egl_window;
			break;
#endif
#if SDL_VERSION_ATLEAST(2, 0, 5) && defined(SDL_VIDEO_DRIVER_VIVANTE)
		case SDL_SYSWM_VIVANTE:
			g_Display = (EGLNativeDisplayType)sysInfo.info.vivante.display;
			g_Window = (EGLNativeWindowType)sysInfo.info.vivante.window;
			break;
#endif
		}

		if (!EGL_OpenInit()) {
			// Let's try again with X11.
			g_Display = (EGLNativeDisplayType)XOpenDisplay(nullptr);
			g_XDisplayOpen = g_Display != nullptr;
			if (!g_XDisplayOpen)
				EGL_ERROR("Unable to get display!", false);
			g_Window = (EGLNativeWindowType)nullptr;
		}
	}

#endif
	if (g_eglDisplay == EGL_NO_DISPLAY)
		EGL_OpenInit();
	return g_eglDisplay == EGL_NO_DISPLAY ? 1 : 0;
}

#ifndef EGL_OPENGL_ES3_BIT_KHR
#define EGL_OPENGL_ES3_BIT_KHR (1 << 6)
#endif

EGLConfig EGL_FindConfig(int *contextVersion) {
	std::vector<EGLConfig> configs;
	EGLint numConfigs = 0;

	EGLBoolean result = eglGetConfigs(g_eglDisplay, nullptr, 0, &numConfigs);
	if (result != EGL_TRUE || numConfigs == 0) {
		return nullptr;
	}

	configs.resize(numConfigs);
	result = eglGetConfigs(g_eglDisplay, &configs[0], numConfigs, &numConfigs);
	if (result != EGL_TRUE || numConfigs == 0) {
		return nullptr;
	}

	// Mali (ARM) seems to have compositing issues with alpha backbuffers.
	// EGL_TRANSPARENT_TYPE doesn't help.
	const char *vendorName = eglQueryString(g_eglDisplay, EGL_VENDOR);
	const bool avoidAlphaGLES = vendorName && !strcmp(vendorName, "ARM");

	EGLConfig best = nullptr;
	int bestScore = 0;
	int bestContextVersion = 0;
	for (const EGLConfig &config : configs) {
		auto readConfig = [&](EGLint attr) -> EGLint {
			EGLint val = 0;
			eglGetConfigAttrib(g_eglDisplay, config, attr, &val);
			return val;
		};

		// We don't want HDR modes with more than 8 bits per component.
		// But let's assume some color is better than no color at all.
		auto readConfigMax = [&](EGLint attr, EGLint m, EGLint def = 1) -> EGLint {
			EGLint val = readConfig(attr);
			return val > m ? def : val;
		};

		int colorScore = readConfigMax(EGL_RED_SIZE, 8) + readConfigMax(EGL_BLUE_SIZE, 8) + readConfigMax(EGL_GREEN_SIZE, 8);
		int alphaScore = readConfigMax(EGL_ALPHA_SIZE, 8);
		int depthScore = readConfig(EGL_DEPTH_SIZE);
		int levelScore = readConfig(EGL_LEVEL) == 0 ? 100 : 0;
		int samplesScore = readConfig(EGL_SAMPLES) == 0 ? 100 : 0;
		int sampleBufferScore = readConfig(EGL_SAMPLE_BUFFERS) == 0 ? 100 : 0;
		int stencilScore = readConfig(EGL_STENCIL_SIZE);
		int transparentScore = readConfig(EGL_TRANSPARENT_TYPE) == EGL_NONE ? 50 : 0;

		EGLint caveat = readConfig(EGL_CONFIG_CAVEAT);
		// Let's assume that non-conformant configs aren't so awful.
		int caveatScore = caveat == EGL_NONE ? 100 : (caveat == EGL_NON_CONFORMANT_CONFIG ? 95 : 0);

#ifndef USING_FBDEV
		EGLint surfaceType = readConfig(EGL_SURFACE_TYPE);
		// Only try a non-Window config in the worst case when there are only non-Window configs.
		int surfaceScore = (surfaceType & EGL_WINDOW_BIT) ? 1000 : 0;
#endif

		EGLint renderable = readConfig(EGL_RENDERABLE_TYPE);
		bool renderableGLES3 = (renderable & EGL_OPENGL_ES3_BIT_KHR) != 0;
		bool renderableGLES2 = (renderable & EGL_OPENGL_ES2_BIT) != 0;
		bool renderableGL = (renderable & EGL_OPENGL_BIT) != 0;
#ifdef USING_GLES2
		int renderableScoreGLES = renderableGLES3 ? 100 : (renderableGLES2 ? 80 : 0);
		int renderableScoreGL = 0;
#else
		int renderableScoreGLES = 0;
		int renderableScoreGL = renderableGL ? 100 : (renderableGLES3 ? 80 : 0);
#endif

		if (avoidAlphaGLES && renderableScoreGLES > 0) {
			alphaScore = 8 - alphaScore;
		}

		int score = 0;
		// Here's a good place to play with the weights to pick a better config.
		score += colorScore * 10 + alphaScore * 2;
		score += depthScore * 5 + stencilScore;
		score += levelScore + samplesScore + sampleBufferScore + transparentScore;
		score += caveatScore + renderableScoreGLES + renderableScoreGL;

#ifndef USING_FBDEV
		score += surfaceScore;
#endif

		if (score > bestScore) {
			bestScore = score;
			best = config;
			bestContextVersion = renderableGLES3 ? 3 : (renderableGLES2 ? 2 : 0);
		}
	}

	*contextVersion = bestContextVersion;
	return best;
}

int8_t EGL_Init(SDL_Window *window) {
	int contextVersion = 0;
	EGLConfig eglConfig = EGL_FindConfig(&contextVersion);
	if (!eglConfig) {
		EGL_ERROR("Unable to find a usable EGL config.", true);
		return 1;
	}

	EGLint contextAttributes[] = {
		EGL_CONTEXT_CLIENT_VERSION, contextVersion,
		EGL_NONE,
	};
	if (contextVersion == 0) {
		contextAttributes[0] = EGL_NONE;
	}

	g_eglContext = eglCreateContext(g_eglDisplay, eglConfig, nullptr, contextAttributes);
	if (g_eglContext == EGL_NO_CONTEXT) {
		EGL_ERROR("Unable to create GLES context!", true);
		return 1;
	}

	g_eglSurface = eglCreateWindowSurface(g_eglDisplay, eglConfig, g_Window, nullptr);
	if (g_eglSurface == EGL_NO_SURFACE) {
		EGL_ERROR("Unable to create EGL surface!", true);
		return 1;
	}

	if (eglMakeCurrent(g_eglDisplay, g_eglSurface, g_eglSurface, g_eglContext) != EGL_TRUE) {
		EGL_ERROR("Unable to make GLES context current.", true);
		return 1;
	}

	return 0;
}

void EGL_Close() {
	if (g_eglDisplay != EGL_NO_DISPLAY) {
		eglMakeCurrent(g_eglDisplay, NULL, NULL, EGL_NO_CONTEXT);
		if (g_eglContext != NULL) {
			eglDestroyContext(g_eglDisplay, g_eglContext);
		}
		if (g_eglSurface != NULL) {
			eglDestroySurface(g_eglDisplay, g_eglSurface);
		}
		eglTerminate(g_eglDisplay);
		g_eglDisplay = EGL_NO_DISPLAY;
	}
	if (g_Display != nullptr) {
#if !defined(USING_FBDEV)
		if (g_XDisplayOpen)
			XCloseDisplay((Display *)g_Display);
#endif
		g_XDisplayOpen = false;
		g_Display = nullptr;
	}
	g_eglSurface = NULL;
	g_eglContext = NULL;
}

#endif // USING_EGL

bool SDLGLGraphicsContext::InitFromRenderThread(std::string *errorMessage) {
	bool retval = GraphicsContext::InitFromRenderThread(errorMessage);
	// HACK: Ensure that the swap interval is set after context creation (needed for kmsdrm)
	SDL_GL_SetSwapInterval(1);
	return retval;
}

// Returns 0 on success.
int SDLGLGraphicsContext::Init(SDL_Window *&window, int x, int y, int w, int h, int mode, std::string *error_message, int force_gl_version) {
	struct GLVersionPair {
		int major;
		int minor;
	};
	GLVersionPair attemptVersions[] = {
#ifdef USING_GLES2
		{3, 2}, {3, 1}, {3, 0}, {2, 0},
#else
		{4, 6}, {4, 5}, {4, 4}, {4, 3}, {4, 2}, {4, 1}, {4, 0},
		{3, 3}, {3, 2}, {3, 1}, {3, 0},
#endif
	};

	// We start hidden because we have to try several windows.
	// On Mac, full screen animates so each attempt is slow.
	mode |= SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN;

	SDL_GLContext glContext = nullptr;
	for (size_t i = 0; i < ARRAY_SIZE(attemptVersions); ++i) {
		const auto &ver = attemptVersions[i];
		// If we force a specific OpenGL version, skip the ones
		// that do not match, which may be all of them - e.g.
		// requesting nonsensical "--graphics=opengl0" reliably
		// skips straight to fallback code below.
		if (force_gl_version >= 0 && 10 * ver.major + ver.minor != force_gl_version)
			continue;
		// Make sure to request a somewhat modern GL context at least - the
		// latest supported by MacOS X (really, really sad...)
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, ver.major);
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, ver.minor);
#ifdef USING_GLES2
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
		SetGLCoreContext(false);
#else
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
		SetGLCoreContext(true);
#endif

		window = SDL_CreateWindow("PPSSPP", x, y, w, h, mode);
		if (!window) {
			// Definitely don't shutdown here: we'll keep trying more GL versions.
			fprintf(stderr, "SDL_CreateWindow failed for GL %d.%d: %s\n", ver.major, ver.minor, SDL_GetError());
			// Skip the DestroyWindow.
			continue;
		}

		glContext = SDL_GL_CreateContext(window);
		if (glContext != nullptr) {
			// Victory, got one.
			break;
		}

		// Let's keep trying.  To be safe, destroy the window - docs say needed to change profile.
		// in practice, it doesn't seem to matter, but maybe it differs by platform.
		SDL_DestroyWindow(window);
	}

	if (glContext == nullptr) {
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, 0);
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 0);
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
		SetGLCoreContext(false);

		window = SDL_CreateWindow("PPSSPP", x, y, w, h, mode);
		if (window == nullptr) {
			NativeShutdown();
			fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError());
			SDL_Quit();
			return 2;
		}

		glContext = SDL_GL_CreateContext(window);
		if (glContext == nullptr) {
			// OK, now we really have tried everything.
			NativeShutdown();
			fprintf(stderr, "SDL_GL_CreateContext failed: %s\n", SDL_GetError());
			SDL_Quit();
			return 2;
		}
	}

	// At this point, we have a window that we can show finally.
	SDL_ShowWindow(window);

#ifdef USING_EGL
	if (EGL_Open(window) != 0) {
		fprintf(stderr, "EGL_Open() failed\n");
	} else if (EGL_Init(window) != 0) {
		fprintf(stderr, "EGL_Init() failed\n");
	} else {
		useEGLSwap = true;
	}
#endif

#ifndef USING_GLES2
	// Some core profile drivers elide certain extensions from GL_EXTENSIONS/etc.
	// glewExperimental allows us to force GLEW to search for the pointers anyway.
	if (gl_extensions.IsCoreContext) {
		glewExperimental = true;
	}
	GLenum glew_err = glewInit();
	// glx is not required, igore.
	if (glew_err != GLEW_OK && glew_err != GLEW_ERROR_NO_GLX_DISPLAY) {
		fprintf(stderr, "Failed to initialize glew!\n");
		return 1;
	}
	// Unfortunately, glew will generate an invalid enum error, ignore.
	if (gl_extensions.IsCoreContext)
		glGetError();

	if (GLEW_VERSION_2_0) {
		fprintf(stderr, "OpenGL 2.0 or higher.\n");
	} else {
		fprintf(stderr, "Sorry, this program requires OpenGL 2.0.\n");
		return 1;
	}
#endif

	// Finally we can do the regular initialization.
	CheckGLExtensions();
	draw_ = Draw::T3DCreateGLContext(true);
	renderManager_ = (GLRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
	renderManager_->SetInflightFrames(g_Config.iInflightFrames);
	SetGPUBackend(GPUBackend::OPENGL);
	bool success = draw_->CreatePresets();
	_assert_(success);
	renderManager_->SetSwapFunction([&]() {
#ifdef USING_EGL
		if (useEGLSwap)
			eglSwapBuffers(g_eglDisplay, g_eglSurface);
		else
			SDL_GL_SwapWindow(window_);
#else
		SDL_GL_SwapWindow(window_);
#endif
	});

	renderManager_->SetSwapIntervalFunction([&](int interval) {
		INFO_LOG(Log::G3D, "SDL SwapInterval: %d", interval);
		SDL_GL_SetSwapInterval(interval);
	});

	window_ = window;
	return 0;
}

void SDLGLGraphicsContext::ShutdownFromRenderThread() {
	delete draw_;
	draw_ = nullptr;
	renderManager_ = nullptr;

#ifdef USING_EGL
	EGL_Close();
#endif
	SDL_GL_DeleteContext(glContext);
	glContext = nullptr;
	window_ = nullptr;
}
