/*
 *    This file is part of the Distant Horizons mod
 *    licensed under the GNU LGPL v3 License.
 *
 *    Copyright (C) 2020 James Seibel
 *
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Lesser General Public License as published by
 *    the Free Software Foundation, version 3.
 *
 *    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 Lesser General Public License for more details.
 *
 *    You should have received a copy of the GNU Lesser General Public License
 *    along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.seibel.distanthorizons.common.render.openGl.glObject;

import com.seibel.distanthorizons.api.enums.config.EDhApiGLErrorHandlingMode;
import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod;
import com.seibel.distanthorizons.api.enums.config.EDhApiLoggerLevel;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.dependencyInjection.ModAccessorInjector;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.jar.EPlatform;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.util.objects.GLMessages.*;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IIrisAccessor;
import com.seibel.distanthorizons.coreapi.ModInfo;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GLCapabilities;
import org.lwjgl.opengl.GLUtil;

import java.io.PrintStream;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * A singleton that holds references to different openGL contexts
 * and GPU capabilities.
 */
public class GLProxy
{
	private static final IIrisAccessor IRIS_ACCESSOR = ModAccessorInjector.INSTANCE.get(IIrisAccessor.class);
	
	public static final DhLogger LOGGER;
	static
	{
		DhLoggerBuilder loggerBuilder = new DhLoggerBuilder();
		loggerBuilder.fileLevelConfig(Config.Common.Logging.logRendererGLEventToFile);
		
		// don't send chat messages if Iris is present since
		// Iris is known to cause (harmless) GL errors
		// and this can confuse users
		boolean irisPresent = (IRIS_ACCESSOR != null);
		if (!irisPresent)
		{
			loggerBuilder.chatLevelConfig(Config.Common.Logging.logRendererGLEventToChat);
		}
		
		LOGGER = loggerBuilder.build();
		
		if (irisPresent)
		{
			LOGGER.info("Iris detected, Distant Horizons OpenGL error logging won't be sent in the chat due to Iris throwing known (harmless) OpenGL errors. This is a bug with Iris, not Distant Horizons.");
		}
	}
	
	public static final Set<String> LOGGED_GL_MESSAGES = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
	
	
	
	private static GLProxy instance = null;
	
	
	/** Minecraft's GL capabilities */
	public final GLCapabilities glCapabilities;
	
	public boolean namedObjectSupported = false; // ~OpenGL 4.5 (UNUSED CURRENTLY)
	public boolean bufferStorageSupported = false; // ~OpenGL 4.4
	public boolean vertexAttributeBufferBindingSupported = false; // ~OpenGL 4.3
	public boolean instancedArraysSupported = false;
	public boolean vertexAttribDivisorSupported = false; // OpenGL 3.3 or newer
	
	private final EDhApiGpuUploadMethod preferredUploadMethod;
	
	public final GLMessageBuilder vanillaDebugMessageBuilder = 
		new GLMessageBuilder(
			(type) ->
			{
				if (type == EGLMessageType.POP_GROUP)
					return false;
				else if (type == EGLMessageType.PUSH_GROUP)
					return false;
				else if (type == EGLMessageType.MARKER)
					return false;
				else
					return true;
			},
			(severity) ->
			{
				// notifications can generally be ignored (if they are logged at all)
				if (severity == EGLMessageSeverity.NOTIFICATION)
					return false;
				else
					return true;
			},
			null
	);
	
	
	
	//=============//
	// constructor //
	//=============//
	//region
	
	private GLProxy() throws IllegalStateException
	{
		// this must be created on minecraft's render context to work correctly
		if (GLFW.glfwGetCurrentContext() == 0L)
		{
			throw new IllegalStateException(GLProxy.class.getSimpleName() + " was created outside the render thread!");
		}
		
		LOGGER.info("Creating " + GLProxy.class.getSimpleName() + "... If this is the last message you see there must have been an OpenGL error.");
		LOGGER.info("Lod Render OpenGL version [" + GL32.glGetString(GL32.GL_VERSION) + "].");
		
		
		
		
		//============================//
		// get Minecraft's GL context //
		//============================//
		
		// get Minecraft's capabilities
		this.glCapabilities = GL.getCapabilities();
		
		// crash the game if the GPU doesn't support OpenGL 3.2
		if (!this.glCapabilities.OpenGL32)
		{
			String supportedVersionInfo = this.getFailedVersionInfo(this.glCapabilities);
			
			// See full requirement at above.
			String errorMessage = ModInfo.READABLE_NAME + " was initializing " + GLProxy.class.getSimpleName()
					+ " and discovered this GPU doesn't meet the OpenGL requirements. Sorry I couldn't tell you sooner :(\n" +
					"Additional info:\n" + supportedVersionInfo;
			IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
			MC.crashMinecraft(errorMessage, new UnsupportedOperationException("Distant Horizon OpenGL requirements not met"));
		}
	 	LOGGER.info("minecraftGlCapabilities:\n" + this.versionInfoToString(this.glCapabilities));
		
		if (Config.Client.Advanced.Debugging.OpenGl.overrideVanillaGLLogger.get())
		{
			GLUtil.setupDebugMessageCallback(new PrintStream(new GLMessageOutputStream(GLProxy::logMessage, this.vanillaDebugMessageBuilder), true));
		}
		
		
		
		//======================//
		// get GPU capabilities //
		//======================//
		
		// UNUSED currently
		// Check if we can use the named version of all calls, which is available in GL4.5 or after
		this.namedObjectSupported = this.glCapabilities.glNamedBufferData != 0L; //Nullptr
		
		// Check if we can use the Buffer Storage, which is available in GL4.4 or after
		this.bufferStorageSupported = this.glCapabilities.glBufferStorage != 0L; // Nullptr
		if (!this.bufferStorageSupported)
		{
			LOGGER.info("This GPU doesn't support Buffer Storage (OpenGL 4.4), falling back to using other methods.");
		}
		
		// Check if we can use the make-over version of Vertex Attribute, which is available in GL4.3 or after
		this.vertexAttributeBufferBindingSupported = this.glCapabilities.glBindVertexBuffer != 0L; // Nullptr
		
		// used by instanced rendering
		this.vertexAttribDivisorSupported = this.glCapabilities.OpenGL33;
		// denotes if ARBInstancedArrays.glVertexAttribDivisorARB() is available or not
		// can be used as a backup if MC didn't create a GL 3.3+ context
		this.instancedArraysSupported = this.glCapabilities.GL_ARB_instanced_arrays;
		
		// get the best automatic upload method
		String vendor = GL32.glGetString(GL32.GL_VENDOR).toUpperCase(); // example return: "NVIDIA CORPORATION"
		if (EPlatform.get() != EPlatform.MACOS)
		{
			if (vendor.contains("NVIDIA") || vendor.contains("GEFORCE"))
			{
				// NVIDIA card
				this.preferredUploadMethod = this.bufferStorageSupported ? EDhApiGpuUploadMethod.BUFFER_STORAGE : EDhApiGpuUploadMethod.SUB_DATA;
			}
			else
			{
				// AMD or Intel card
				this.preferredUploadMethod = this.bufferStorageSupported ? EDhApiGpuUploadMethod.BUFFER_STORAGE : EDhApiGpuUploadMethod.DATA;
			}
		}
		else
		{
			// Mac may have an issue with Buffer Storage, so default to the most basic
			// form of uploading
			this.preferredUploadMethod = EDhApiGpuUploadMethod.DATA;
		}
		LOGGER.info("GPU Vendor [" + vendor + "] with OS [" + EPlatform.get().getName() + "], Preferred upload method is [" + this.preferredUploadMethod + "].");
		
		
		
		//==========//
		// clean up //
		//==========//
		
		// GLProxy creation success
		LOGGER.info(GLProxy.class.getSimpleName() + " creation successful. OpenGL smiles upon you this day.");
	}
	
	//endregion
	
	
	
	//=========//
	// getters //
	//=========//
	//region
	
	public static boolean hasInstance() { return instance != null; }
	/** @throws IllegalStateException if the Proxy hasn't been created yet and this is called outside the render thread */
	public static GLProxy getInstance() throws IllegalStateException
	{
		if (instance == null)
		{
			instance = new GLProxy();
		}
		
		return instance;
	}
	
	public EDhApiGpuUploadMethod getGpuUploadMethod() { return this.preferredUploadMethod; }
	
	public static boolean runningOnRenderThread()
	{
		long currentContext = GLFW.glfwGetCurrentContext();
		return currentContext != 0L; // if the context isn't null, it's the MC context
	}
	
	//endregion
	
	
	
	//=========//
	// logging //
	//=========//
	//region
	
	/** this method is called on the render thread at the point of the GL Error */
	private static void logMessage(GLMessage glMessage)
	{
		EDhApiGLErrorHandlingMode errorHandlingMode = Config.Client.Advanced.Debugging.OpenGl.glErrorHandlingMode.get();
		if (errorHandlingMode == EDhApiGLErrorHandlingMode.IGNORE)
		{
			return;
		}
		
		
		
		boolean onlyLogOnce = Config.Client.Advanced.Debugging.OpenGl.onlyLogGlErrorsOnce.get();
		if (onlyLogOnce
			&& !LOGGED_GL_MESSAGES.add(glMessage.message))
		{
			// this message has already been logged
			return;
		}
		
		String errorMessage = "GL ERROR [" + glMessage.id + "] from [" + glMessage.source + "]: [" + glMessage.message + "].";
		if (onlyLogOnce)
		{
			errorMessage += " This message will only be logged once.";
			errorMessage += " Note: Distant Horizons will catch and log OpenGL errors from other mods, not just DH itself; if everything is rendering correctly these errors can probably be ignored.";
		}
		
		
		
		// create an exception so we get a stacktrace of where the message was triggered from
		RuntimeException exception = new RuntimeException(errorMessage);
		
		if (glMessage.type == EGLMessageType.ERROR || glMessage.type == EGLMessageType.UNDEFINED_BEHAVIOR)
		{
			// critical error
			
			LOGGER.error(exception.getMessage(), exception);
			
			if (errorHandlingMode == EDhApiGLErrorHandlingMode.LOG_THROW)
			{
				// will probably crash the game,
				// good for quickly checking if there's a problem while preventing log spam
				throw exception;
			}
		}
		else
		{
			// non-critical log
			
			EGLMessageSeverity severity = glMessage.severity;
			if (severity == null)
			{
				// just in case the message was malformed
				severity = EGLMessageSeverity.LOW;
			}
			
			switch (severity)
			{
				case HIGH:
					LOGGER.error(exception.getMessage(), exception);
					break;
				case MEDIUM:
					LOGGER.warn(exception.getMessage(), exception);
					break;
				case LOW:
					LOGGER.info(exception.getMessage(), exception);
					break;
				case NOTIFICATION:
					LOGGER.debug(exception.getMessage(), exception);
					break;
			}
		}
	}
	
	//endregion
	
	
	
	//================//
	// helper methods //
	//================//
	//region
	
	private String getFailedVersionInfo(GLCapabilities c)
	{
		return "Your OpenGL support:\n" +
				"openGL version 3.2+: [" + c.OpenGL32 + "] <- REQUIRED\n" +
				"Vertex Attribute Buffer Binding: [" + (c.glVertexAttribBinding != 0) + "] <- optional improvement\n" +
				"Buffer Storage: [" + (c.glBufferStorage != 0) + "] <- optional improvement\n" +
				"If you noticed that your computer supports higher OpenGL versions"
				+ " but not the required version, try running the game in compatibility mode."
				+ " (How you turn that on, I have no clue~)";
	}
	
	private String versionInfoToString(GLCapabilities c)
	{
		return "Your OpenGL support:\n" +
				"openGL version 3.2+: [" + c.OpenGL32 + "] <- REQUIRED\n" +
				"Vertex Attribute Buffer Binding: [" + (c.glVertexAttribBinding != 0) + "] <- optional improvement\n" +
				"Buffer Storage: [" + (c.glBufferStorage != 0) + "] <- optional improvement\n";
	}
	
	//endregion
	
	
	
}
