/*
 * Copyright (c) 2018, Adam <Adam@sigterm.info>
 * Copyright (c) 2021, 117 <https://twitter.com/117scape>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package rs117.hd;

import com.google.common.primitives.Ints;
import com.google.inject.Provides;
import com.jogamp.nativewindow.AbstractGraphicsConfiguration;
import com.jogamp.nativewindow.NativeWindowFactory;
import com.jogamp.nativewindow.awt.AWTGraphicsConfiguration;
import com.jogamp.nativewindow.awt.JAWTWindow;
import com.jogamp.opengl.*;
import com.jogamp.opengl.math.Matrix4;
import javax.inject.Named;
import jogamp.nativewindow.SurfaceScaleUtils;
import jogamp.nativewindow.jawt.x11.X11JAWTWindow;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.*;
import net.runelite.api.events.*;
import net.runelite.api.hooks.DrawCallbacks;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.plugins.*;
import net.runelite.client.plugins.entityhider.EntityHiderPlugin;
import net.runelite.client.ui.DrawManager;
import net.runelite.client.util.OSType;
import org.jocl.CL;
import static rs117.hd.HdPluginConfig.KEY_WINTER_THEME;
import rs117.hd.config.AntiAliasingMode;
import rs117.hd.config.DefaultSkyColor;
import rs117.hd.config.FogDepthMode;
import rs117.hd.config.UIScalingMode;
import rs117.hd.data.materials.Material;
import rs117.hd.model.ModelHasher;
import rs117.hd.model.ModelPusher;
import rs117.hd.model.TempModelInfo;
import rs117.hd.model.objects.ObjectProperties;
import rs117.hd.model.objects.ObjectType;
import rs117.hd.opengl.compute.ComputeMode;
import rs117.hd.opengl.compute.OpenCLManager;
import rs117.hd.opengl.shader.Shader;
import rs117.hd.opengl.shader.ShaderException;
import rs117.hd.opengl.shader.Template;
import rs117.hd.scene.EnvironmentManager;
import rs117.hd.scene.ProceduralGenerator;
import rs117.hd.scene.SceneUploader;
import rs117.hd.scene.TextureManager;
import rs117.hd.scene.lighting.LightManager;
import rs117.hd.scene.lighting.SceneLight;
import rs117.hd.utils.*;
import rs117.hd.utils.buffer.GLBuffer;
import rs117.hd.utils.buffer.GpuFloatBuffer;
import rs117.hd.utils.buffer.GpuIntBuffer;

import javax.inject.Inject;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.nio.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import static com.jogamp.opengl.GL.*;
import static com.jogamp.opengl.GL2ES2.GL_STREAM_DRAW;
import static com.jogamp.opengl.GL2ES3.GL_STATIC_COPY;
import static com.jogamp.opengl.GL2ES3.GL_UNIFORM_BUFFER;
import static org.jocl.CL.*;
import static rs117.hd.utils.GLUtil.*;

@PluginDescriptor(
	name = "117 HD (beta)",
	description = "GPU renderer with a suite of graphical enhancements",
	tags = {"hd", "high", "detail", "graphics", "shaders", "textures", "gpu", "shadows", "lights"},
	conflicts = "GPU"
)
@PluginDependency(EntityHiderPlugin.class)
@Slf4j
public class HdPlugin extends Plugin implements DrawCallbacks
{
	// This is the maximum number of triangles the compute shaders support
	public static final int MAX_TRIANGLE = 6144;
	public static final int SMALL_TRIANGLE_COUNT = 512;
	private static final int FLAG_SCENE_BUFFER = Integer.MIN_VALUE;
	private static final int DEFAULT_DISTANCE = 25;
	static final int MAX_DISTANCE = 90;
	static final int MAX_FOG_DEPTH = 100;
	// MAX_MATERIALS and MAX_LIGHTS must match the #defined values in the HD and shadow fragment shaders
	private static final int MAX_MATERIALS = Material.values().length;
	private static final int MAX_LIGHTS = 100;
	private static final int MATERIAL_PROPERTIES_COUNT = 12;
	private static final int LIGHT_PROPERTIES_COUNT = 8;
	private static final int SCALAR_BYTES = 4;

	private static final int[] eightIntWrite = new int[8];

	@Inject
	private Client client;
	
	@Inject
	private OpenCLManager openCLManager;

	@Inject
	private ClientThread clientThread;

	@Inject
	private HdPluginConfig config;

	@Inject
	private TextureManager textureManager;

	@Inject
	private LightManager lightManager;

	@Inject
	private EnvironmentManager environmentManager;

	@Inject
	private SceneUploader sceneUploader;

	@Inject
	private DrawManager drawManager;

	@Inject
	private PluginManager pluginManager;

	@Inject
	private ProceduralGenerator proceduralGenerator;

	@Inject
	private ConfigManager configManager;

	@Inject
	private ModelPusher modelPusher;

	@Inject
	private ModelHasher modelHasher;

	@Inject
	@Named("developerMode")
	private boolean developerMode;

	@Inject
	private DeveloperTools developerTools;

	private ComputeMode computeMode = ComputeMode.OPENGL;

	private Canvas canvas;
	private JAWTWindow jawtWindow;
	private GL4 gl;
	private GLContext glContext;
	private GLDrawable glDrawable;

	static final String LINUX_VERSION_HEADER =
		"#version 420\n" +
			"#extension GL_ARB_compute_shader : require\n" +
			"#extension GL_ARB_shader_storage_buffer_object : require\n" +
			"#extension GL_ARB_explicit_attrib_location : require\n";
	static final String WINDOWS_VERSION_HEADER = "#version 430\n";

	static final Shader PROGRAM = new Shader()
		.add(GL4.GL_VERTEX_SHADER, "vert.glsl")
		.add(GL4.GL_GEOMETRY_SHADER, "geom.glsl")
		.add(GL4.GL_FRAGMENT_SHADER, "frag.glsl");

	static final Shader SHADOW_PROGRAM = new Shader()
		.add(GL4.GL_VERTEX_SHADER, "shadow_vert.glsl")
		.add(GL4.GL_FRAGMENT_SHADER, "shadow_frag.glsl");

	static final Shader COMPUTE_PROGRAM = new Shader()
		.add(GL4.GL_COMPUTE_SHADER, "comp.glsl");

	static final Shader SMALL_COMPUTE_PROGRAM = new Shader()
		.add(GL4.GL_COMPUTE_SHADER, "comp_small.glsl");

	static final Shader UNORDERED_COMPUTE_PROGRAM = new Shader()
		.add(GL4.GL_COMPUTE_SHADER, "comp_unordered.glsl");

	static final Shader UI_PROGRAM = new Shader()
		.add(GL4.GL_VERTEX_SHADER, "vertui.glsl")
		.add(GL4.GL_FRAGMENT_SHADER, "fragui.glsl");

	private int glProgram = -1;
	private int glComputeProgram = -1;
	private int glSmallComputeProgram = -1;
	private int glUnorderedComputeProgram = -1;
	private int glUiProgram = -1;
	private int glShadowProgram = -1;

	private int vaoHandle = -1;

	private int interfaceTexture = -1;
	private int interfacePbo = -1;

	private int vaoUiHandle = -1;
	private int vboUiHandle = -1;

	private int fboSceneHandle = -1;
	private int rboSceneHandle = -1;

	private int fboShadowMap = -1;
	private int texShadowMap = -1;

	// scene vertex buffer
	private final GLBuffer sceneVertexBuffer = new GLBuffer();
	// scene uv buffer
	private final GLBuffer sceneUvBuffer = new GLBuffer();
	// scene normal buffer
	private final GLBuffer sceneNormalBuffer = new GLBuffer();

	private final GLBuffer tmpVertexBuffer = new GLBuffer(); // temporary scene vertex buffer
	private final GLBuffer tmpUvBuffer = new GLBuffer(); // temporary scene uv buffer
	private final GLBuffer tmpNormalBuffer = new GLBuffer(); // temporary scene normal buffer
	private final GLBuffer tmpModelBufferLarge = new GLBuffer(); // scene model buffer, large
	private final GLBuffer tmpModelBufferSmall = new GLBuffer(); // scene model buffer, small
	private final GLBuffer tmpModelBufferUnordered = new GLBuffer(); // scene model buffer, unordered
	private final GLBuffer tmpOutBuffer = new GLBuffer(); // target vertex buffer for compute shaders
	private final GLBuffer tmpOutUvBuffer = new GLBuffer(); // target uv buffer for compute shaders
	private final GLBuffer tmpOutNormalBuffer = new GLBuffer(); // target normal buffer for compute shaders

	private int textureArrayId;
	private int textureHDArrayId;

	private final GLBuffer uniformBuffer = new GLBuffer();
	private final float[] textureOffsets = new float[256];
	private final GLBuffer materialsUniformBuffer = new GLBuffer();
	private final GLBuffer lightsUniformBuffer = new GLBuffer();
	private ByteBuffer lightsUniformBuf;

	private GpuIntBuffer vertexBuffer;
	private GpuFloatBuffer uvBuffer;
	private GpuFloatBuffer normalBuffer;

	private GpuIntBuffer modelBufferUnordered;
	private GpuIntBuffer modelBufferSmall;
	private GpuIntBuffer modelBuffer;

	private int unorderedModels;

	/**
	 * number of models in small buffer
	 */
	private int smallModels;

	/**
	 * number of models in large buffer
	 */
	private int largeModels;

	/**
	 * offset in the target buffer for model
	 */
	private int targetBufferOffset;

	/**
	 * offset into the temporary scene vertex buffer
	 */
	private int tempOffset;

	/**
	 * offset into the temporary scene uv buffer
	 */
	private int tempUvOffset;

	private int lastCanvasWidth;
	private int lastCanvasHeight;
	private int lastStretchedCanvasWidth;
	private int lastStretchedCanvasHeight;
	private AntiAliasingMode lastAntiAliasingMode;
	private int lastAnisotropicFilteringLevel = -1;

	private int yaw;
	private int pitch;
	private int viewportOffsetX;
	private int viewportOffsetY;

	// Uniforms
	private int uniColorBlindMode;
	private int uniUiColorBlindMode;
	private int uniUseFog;
	private int uniFogColor;
	private int uniFogDepth;
	private int uniDrawDistance;
	private int uniWaterColorLight;
	private int uniWaterColorMid;
	private int uniWaterColorDark;
	private int uniAmbientStrength;
	private int uniAmbientColor;
	private int uniLightStrength;
	private int uniLightColor;
	private int uniUnderglowStrength;
	private int uniUnderglowColor;
	private int uniGroundFogStart;
	private int uniGroundFogEnd;
	private int uniGroundFogOpacity;
	private int uniLightningBrightness;
	private int uniSaturation;
	private int uniContrast;
	private int uniLightX;
	private int uniLightY;
	private int uniLightZ;
	private int uniShadowMaxBias;
	private int uniShadowsEnabled;
	private int uniUnderwaterEnvironment;
	private int uniUnderwaterCaustics;
	private int uniUnderwaterCausticsColor;
	private int uniUnderwaterCausticsStrength;

	// Shadow program uniforms
	private int uniShadowTexturesHD;
	private int uniShadowTextureOffsets;
	private int uniShadowLightProjectionMatrix;

	// Point light uniforms
	private int uniPointLightsCount;

	private int uniProjectionMatrix;
	private int uniLightProjectionMatrix;
	private int uniShadowMap;
	private int uniTex;
	private int uniTexSamplingMode;
	private int uniTexSourceDimensions;
	private int uniTexTargetDimensions;
	private int uniUiAlphaOverlay;
	private int uniTextures;
	private int uniTexturesHD;
	private int uniTextureOffsets;
	private int uniAnimationCurrent;

	private int uniBlockSmall;
	private int uniBlockLarge;
	private int uniBlockMain;
	private int uniBlockMaterials;
	private int uniShadowBlockMaterials;
	private int uniBlockPointLights;

	// Animation things
	private long lastFrameTime = System.currentTimeMillis();
	// Generic scalable animation timer used in shaders
	private float animationCurrent = 0;

	// future time to reload the scene
	// useful for pulling new data into the scene buffer
	@Setter
	private long nextSceneReload = 0;

	// some necessary data for reloading the scene while in POH to fix major performance loss
	@Setter
	private boolean isInHouse = false;
	private int previousPlane;

	// Config settings used very frequently - thousands/frame
	public boolean configGroundTextures = false;
	public boolean configGroundBlending = false;
	public boolean configObjectTextures = true;
	public boolean configTzhaarHD = true;
	public boolean configProjectileLights = true;
	public boolean configNpcLights = true;
	public boolean configShadowsEnabled = false;
	public boolean configExpandShadowDraw = false;
	public boolean configHdInfernalTexture = true;
	public boolean configWinterTheme = true;

	public int[] camTarget = new int[3];

	private int needsReset;
	private boolean hasLoggedIn;

	@Setter
	private boolean isInGauntlet = false;

	private final Map<Integer, TempModelInfo> tempModelInfoMap = new HashMap<>();

	@Subscribe
	public void onChatMessage(final ChatMessage event) {
		if (!isInGauntlet) {
			return;
		}

		// reload the scene if the player is in the gauntlet and opening a new room to pull the new data into the buffer
		if (event.getMessage().equals("You light the nodes in the corridor to help guide the way.")) {
			reloadScene();
		}
	}

	private final ComponentListener resizeListener = new ComponentAdapter()
	{
		@Override
		public void componentResized(ComponentEvent e)
		{
			// forward to the JAWTWindow component listener on the canvas. The JAWTWindow component
			// listener listens for resizes or movement of the component in order to resize and move
			// the associated offscreen layer (calayer on macos only)
			canvas.dispatchEvent(e);
			// resetSize needs to be run awhile after the resize is completed.
			// I've tried waiting until all EDT events are completed and even that is too soon.
			// Not sure why, so we just wait a few frames.
			needsReset = 5;
		}
	};

	@Override
	protected void startUp()
	{
		configGroundTextures = config.groundTextures();
		configGroundBlending = config.groundBlending();
		configObjectTextures = config.objectTextures();
		configTzhaarHD = config.tzhaarHD();
		configProjectileLights = config.projectileLights();
		configNpcLights = config.npcLights();
		configShadowsEnabled = config.shadowsEnabled();
		configExpandShadowDraw = config.expandShadowDraw();
		configHdInfernalTexture = config.hdInfernalTexture();
		configWinterTheme = config.winterTheme();

		clientThread.invoke(() ->
		{
			try
			{
				targetBufferOffset = 0;
				fboSceneHandle = rboSceneHandle = -1; // AA FBO
				fboShadowMap = -1;
				unorderedModels = smallModels = largeModels = 0;

				canvas = client.getCanvas();

				if (!canvas.isDisplayable())
				{
					return false;
				}
				
				computeMode = OSType.getOSType() == OSType.MacOS ? ComputeMode.OPENCL : ComputeMode.OPENGL;

				canvas.setIgnoreRepaint(true);

				vertexBuffer = new GpuIntBuffer();
				uvBuffer = new GpuFloatBuffer();
				normalBuffer = new GpuFloatBuffer();

				modelBufferUnordered = new GpuIntBuffer();
				modelBufferSmall = new GpuIntBuffer();
				modelBuffer = new GpuIntBuffer();

				if (log.isDebugEnabled())
				{
					System.setProperty("jogl.debug", "true");
				}
				
				System.setProperty("jogamp.gluegen.TestTempDirExec", "false");

				if (developerMode)
				{
					developerTools.activate();
				}

				GLProfile.initSingleton();

				ThreadUtils.invokeOnMainThread(() ->
				{
					GLProfile glProfile;
					GLCapabilities glCaps;
					try {
						glProfile = GLProfile.get(GLProfile.GL4);
						glCaps = new GLCapabilities(glProfile);

						// Get and display the device and driver used by the GPU plugin
						GLDrawable dummyDrawable = GLDrawableFactory.getFactory(glProfile)
								.createDummyDrawable(GLProfile.getDefaultDevice(), true, glCaps, null);
						dummyDrawable.setRealized(true);
						GLContext versionContext = dummyDrawable.createContext(null);
						versionContext.makeCurrent();
						GL versionGL = versionContext.getGL();
						log.info("Using device: {}", versionGL.glGetString(GL.GL_RENDERER));
						log.info("Using driver: {}", versionGL.glGetString(GL.GL_VERSION));
						log.info("Client is {}-bit", System.getProperty("sun.arch.data.model"));
						versionContext.destroy();
					} catch (Exception ex) {
						log.error("failed to get device information", ex);
						stopPlugin();
						return;
					}

					AWTGraphicsConfiguration config = AWTGraphicsConfiguration.create(canvas.getGraphicsConfiguration(), glCaps, glCaps);

					jawtWindow = (JAWTWindow) NativeWindowFactory.getNativeWindow(canvas, config);
					canvas.setFocusable(true);

					GLDrawableFactory glDrawableFactory = GLDrawableFactory.getFactory(glProfile);

					jawtWindow.lockSurface();
					try
					{
						glDrawable = glDrawableFactory.createGLDrawable(jawtWindow);
						glDrawable.setRealized(true);

						glContext = glDrawable.createContext(null);
						if (log.isDebugEnabled())
						{
							// Debug config on context needs to be set before .makeCurrent call
							glContext.enableGLDebugMessage(true);
						}
					}
					finally
					{
						jawtWindow.unlockSurface();
					}

					int res = glContext.makeCurrent();
					if (res == GLContext.CONTEXT_NOT_CURRENT)
					{
						throw new GLException("Unable to make context current");
					}

					// Surface needs to be unlocked on X11 window otherwise input is blocked
					if (jawtWindow instanceof X11JAWTWindow && jawtWindow.getLock().isLocked())
					{
						jawtWindow.unlockSurface();
					}

					this.gl = glContext.getGL().getGL4();

					setupSyncMode();

					if (log.isDebugEnabled())
					{
						try
						{
							gl = new DebugGL4(gl);
						}
						catch (NoClassDefFoundError ex)
						{
							log.debug("Disabling DebugGL due to jogl-gldesktop-dbg not being present on the classpath");
						}

						gl.glEnable(gl.GL_DEBUG_OUTPUT);

						//	GLDebugEvent[ id 0x20071
						//		type Warning: generic
						//		severity Unknown (0x826b)
						//		source GL API
						//		msg Buffer detailed info: Buffer object 11 (bound to GL_ARRAY_BUFFER_ARB, and GL_SHADER_STORAGE_BUFFER (4), usage hint is GL_STREAM_DRAW) will use VIDEO memory as the source for buffer object operations.
						glContext.glDebugMessageControl(gl.GL_DEBUG_SOURCE_API, gl.GL_DEBUG_TYPE_OTHER,
							gl.GL_DONT_CARE, 1, new int[]{0x20071}, 0, false);

						//	GLDebugMessageHandler: GLDebugEvent[ id 0x20052
						//		type Warning: implementation dependent performance
						//		severity Medium: Severe performance/deprecation/other warnings
						//		source GL API
						//		msg Pixel-path performance warning: Pixel transfer is synchronized with 3D rendering.
						glContext.glDebugMessageControl(gl.GL_DEBUG_SOURCE_API, gl.GL_DEBUG_TYPE_PERFORMANCE,
							gl.GL_DONT_CARE, 1, new int[]{0x20052}, 0, false);
					}

					lastFrameTime = System.currentTimeMillis();

					initVao();
					try
					{
						initPrograms();
					}
					catch (ShaderException ex)
					{
						throw new RuntimeException(ex);
					}
					initInterfaceTexture();
					initUniformBuffer();
					initMaterialsUniformBuffer();
					initLightsUniformBuffer();
					initBuffers();
					initShadowMapFbo();
				});

				client.setDrawCallbacks(this);
				client.setGpu(true);

				// force rebuild of main buffer provider to enable alpha channel
				client.resizeCanvas();

				lastCanvasWidth = lastCanvasHeight = -1;
				lastStretchedCanvasWidth = lastStretchedCanvasHeight = -1;
				lastAntiAliasingMode = null;

				textureArrayId = -1;
				textureHDArrayId = -1;

				// load all dynamic scene lights from text file
				lightManager.startUp();

				if (client.getGameState() == GameState.LOGGED_IN)
				{
					ThreadUtils.invokeOnMainThread(this::uploadScene);
				}

				if (OSType.getOSType() == OSType.MacOS)
				{
					SwingUtilities.invokeAndWait(() -> ((Component) client).addComponentListener(resizeListener));
					needsReset = 5; // plugin startup races with ClientUI positioning, so do a reset in a little bit
				}
			}
			catch (Throwable e)
			{
				log.error("Error starting HD plugin", e);
				stopPlugin();
			}
			return true;
		});
	}

	@Override
	protected void shutDown()
	{
		developerTools.deactivate();

		((Component) client).removeComponentListener(resizeListener);
		lightManager.shutDown();

		clientThread.invoke(() ->
		{
			client.setGpu(false);
			client.setDrawCallbacks(null);
			client.setUnlockedFps(false);

			ThreadUtils.invokeOnMainThread(() ->
			{
				openCLManager.cleanup();
				
				if (gl != null)
				{
					if (textureArrayId != -1)
					{
						textureManager.freeTextureArray(gl, textureArrayId);
						textureArrayId = -1;
					}

					if (textureHDArrayId != -1)
					{
						textureManager.freeTextureArray(gl, textureHDArrayId);
						textureHDArrayId = -1;
					}

					destroyGlBuffer(uniformBuffer);
					destroyGlBuffer(materialsUniformBuffer);
					destroyGlBuffer(lightsUniformBuffer);

					shutdownBuffers();
					shutdownInterfaceTexture();
					shutdownPrograms();
					shutdownVao();
					shutdownAAFbo();
					shutdownShadowMapFbo();
				}

				if (jawtWindow != null)
				{
					if (!jawtWindow.getLock().isLocked())
					{
						jawtWindow.lockSurface();
					}

					if (glContext != null)
					{
						glContext.destroy();
					}

					// this crashes on osx when the plugin is turned back on, don't know why
					// we'll just leak the window...
					if (OSType.getOSType() != OSType.MacOS)
					{
						final AbstractGraphicsConfiguration config = jawtWindow.getGraphicsConfiguration();
						jawtWindow.destroy();
						config.getScreen().getDevice().close();
					}
				}
			});

			GLProfile.shutdown();

			jawtWindow = null;
			gl = null;
			glDrawable = null;
			glContext = null;

			vertexBuffer = null;
			uvBuffer = null;
			normalBuffer = null;

			modelBufferSmall = null;
			modelBuffer = null;
			modelBufferUnordered = null;

			lastAnisotropicFilteringLevel = -1;

			// force main buffer provider rebuild to turn off alpha channel
			client.resizeCanvas();
		});
	}

	private void stopPlugin()
	{
		SwingUtilities.invokeLater(() ->
		{
			try
			{
				pluginManager.setPluginEnabled(this, false);
				pluginManager.stopPlugin(this);
			}
			catch (PluginInstantiationException ex)
			{
				log.error("error stopping plugin", ex);
			}
		});

		shutDown();
	}

	@Provides
	HdPluginConfig provideConfig(ConfigManager configManager)
	{
		return configManager.getConfig(HdPluginConfig.class);
	}

	private String generateFetchMaterialCases(int from, int to)
	{
		int length = to - from;
		if (length == 1)
		{
			return "material[" + from + "]";
		}
		int middle = from + length / 2;
		return "i < " + middle +
			" ? " + generateFetchMaterialCases(from, middle) +
			" : " + generateFetchMaterialCases(middle, to);
	}

	private void initPrograms() throws ShaderException
	{
		String versionHeader = OSType.getOSType() == OSType.Linux ? LINUX_VERSION_HEADER : WINDOWS_VERSION_HEADER;
		Template template = new Template();
		template.add(key ->
		{
			switch (key)
			{
				case "version_header":
					return versionHeader;
				case "MAX_MATERIALS":
					return String.format("#define %s %d\n", key, MAX_MATERIALS);
				case "CONST_MACOS_INTEL_WORKAROUND":
					boolean isAppleM1 = OSType.getOSType() == OSType.MacOS && System.getProperty("os.arch").equals("aarch64");
					return String.format("#define %s %d\n", key, config.macosIntelWorkaround() && !isAppleM1 ? 1 : 0);
				case "MACOS_INTEL_WORKAROUND_MATERIAL_CASES":
					return "return " + generateFetchMaterialCases(0, MAX_MATERIALS) + ";";
			}
			return null;
		});
		if (developerMode)
		{
			template.add(developerTools::shaderResolver);
		}
		template.addInclude(HdPlugin.class);

		glProgram = PROGRAM.compile(gl, template);
		glUiProgram = UI_PROGRAM.compile(gl, template);
		glShadowProgram = SHADOW_PROGRAM.compile(gl, template);
		
		if (computeMode == ComputeMode.OPENCL)
		{
			openCLManager.init(gl);
		}
		else
		{
			glComputeProgram = COMPUTE_PROGRAM.compile(gl, template);
			glSmallComputeProgram = SMALL_COMPUTE_PROGRAM.compile(gl, template);
			glUnorderedComputeProgram = UNORDERED_COMPUTE_PROGRAM.compile(gl, template);
		}

		initUniforms();

		gl.glUseProgram(glProgram);

		// bind texture samplers before validating, else the validation fails
		gl.glUniform1i(uniTextures, 1); // texture sampler array is bound to texture1
		gl.glUniform1i(uniTexturesHD, 2); // HD texture sampler array is bound to texture2
		gl.glUniform1i(uniShadowMap, 3); // shadow map sampler is bound to texture3

		gl.glUseProgram(0);

		// validate program
		gl.glValidateProgram(glProgram);

		if (GLUtil.glGetProgram(gl, glProgram, gl.GL_VALIDATE_STATUS) == gl.GL_FALSE)
		{
			String err = GLUtil.glGetProgramInfoLog(gl, glProgram);
			throw new ShaderException(err);
		}
	}

	private void initUniforms()
	{
		uniProjectionMatrix = gl.glGetUniformLocation(glProgram, "projectionMatrix");
		uniLightProjectionMatrix = gl.glGetUniformLocation(glProgram, "lightProjectionMatrix");
		uniShadowMap = gl.glGetUniformLocation(glProgram, "shadowMap");
		uniSaturation = gl.glGetUniformLocation(glProgram, "saturation");
		uniContrast = gl.glGetUniformLocation(glProgram, "contrast");
		uniUseFog = gl.glGetUniformLocation(glProgram, "useFog");
		uniFogColor = gl.glGetUniformLocation(glProgram, "fogColor");
		uniFogDepth = gl.glGetUniformLocation(glProgram, "fogDepth");
		uniWaterColorLight = gl.glGetUniformLocation(glProgram, "waterColorLight");
		uniWaterColorMid = gl.glGetUniformLocation(glProgram, "waterColorMid");
		uniWaterColorDark = gl.glGetUniformLocation(glProgram, "waterColorDark");
		uniDrawDistance = gl.glGetUniformLocation(glProgram, "drawDistance");
		uniAmbientStrength = gl.glGetUniformLocation(glProgram, "ambientStrength");
		uniAmbientColor = gl.glGetUniformLocation(glProgram, "ambientColor");
		uniLightStrength = gl.glGetUniformLocation(glProgram, "lightStrength");
		uniLightColor = gl.glGetUniformLocation(glProgram, "lightColor");
		uniUnderglowStrength = gl.glGetUniformLocation(glProgram, "underglowStrength");
		uniUnderglowColor = gl.glGetUniformLocation(glProgram, "underglowColor");
		uniGroundFogStart = gl.glGetUniformLocation(glProgram, "groundFogStart");
		uniGroundFogEnd = gl.glGetUniformLocation(glProgram, "groundFogEnd");
		uniGroundFogOpacity = gl.glGetUniformLocation(glProgram, "groundFogOpacity");
		uniLightningBrightness = gl.glGetUniformLocation(glProgram, "lightningBrightness");
		uniPointLightsCount = gl.glGetUniformLocation(glProgram, "pointLightsCount");
		uniColorBlindMode = gl.glGetUniformLocation(glProgram, "colorBlindMode");
		uniLightX = gl.glGetUniformLocation(glProgram, "lightX");
		uniLightY = gl.glGetUniformLocation(glProgram, "lightY");
		uniLightZ = gl.glGetUniformLocation(glProgram, "lightZ");
		uniShadowMaxBias = gl.glGetUniformLocation(glProgram, "shadowMaxBias");
		uniShadowsEnabled = gl.glGetUniformLocation(glProgram, "shadowsEnabled");
		uniUnderwaterEnvironment = gl.glGetUniformLocation(glProgram, "underwaterEnvironment");
		uniUnderwaterCaustics = gl.glGetUniformLocation(glProgram, "underwaterCaustics");
		uniUnderwaterCausticsColor = gl.glGetUniformLocation(glProgram, "underwaterCausticsColor");
		uniUnderwaterCausticsStrength = gl.glGetUniformLocation(glProgram, "underwaterCausticsStrength");

		uniTex = gl.glGetUniformLocation(glUiProgram, "tex");
		uniTexSamplingMode = gl.glGetUniformLocation(glUiProgram, "samplingMode");
		uniTexTargetDimensions = gl.glGetUniformLocation(glUiProgram, "targetDimensions");
		uniTexSourceDimensions = gl.glGetUniformLocation(glUiProgram, "sourceDimensions");
		uniUiColorBlindMode = gl.glGetUniformLocation(glUiProgram, "colorBlindMode");
		uniUiAlphaOverlay = gl.glGetUniformLocation(glUiProgram, "alphaOverlay");
		uniTextures = gl.glGetUniformLocation(glProgram, "textures");
		uniTexturesHD = gl.glGetUniformLocation(glProgram, "texturesHD");
		uniTextureOffsets = gl.glGetUniformLocation(glProgram, "textureOffsets");
		uniAnimationCurrent = gl.glGetUniformLocation(glProgram, "animationCurrent");

		uniBlockSmall = gl.glGetUniformBlockIndex(glSmallComputeProgram, "uniforms");
		uniBlockLarge = gl.glGetUniformBlockIndex(glComputeProgram, "uniforms");
		uniBlockMain = gl.glGetUniformBlockIndex(glProgram, "uniforms");
		uniBlockMaterials = gl.glGetUniformBlockIndex(glProgram, "materials");
		uniBlockPointLights = gl.glGetUniformBlockIndex(glProgram, "pointLights");

		// Shadow program uniforms
		uniShadowBlockMaterials = gl.glGetUniformBlockIndex(glShadowProgram, "materials");
		uniShadowLightProjectionMatrix = gl.glGetUniformLocation(glShadowProgram, "lightProjectionMatrix");
		uniShadowTexturesHD = gl.glGetUniformLocation(glShadowProgram, "texturesHD");
		uniShadowTextureOffsets = gl.glGetUniformLocation(glShadowProgram, "textureOffsets");
	}

	private void shutdownPrograms()
	{
		if (glProgram != -1)
		{
			gl.glDeleteProgram(glProgram);
			glProgram = -1;
		}

		if (glComputeProgram != -1)
		{
			gl.glDeleteProgram(glComputeProgram);
			glComputeProgram = -1;
		}

		if (glSmallComputeProgram != -1)
		{
			gl.glDeleteProgram(glSmallComputeProgram);
			glSmallComputeProgram = -1;
		}

		if (glUnorderedComputeProgram != -1)
		{
			gl.glDeleteProgram(glUnorderedComputeProgram);
			glUnorderedComputeProgram = -1;
		}

		if (glUiProgram != -1)
		{
			gl.glDeleteProgram(glUiProgram);
			glUiProgram = -1;
		}

		if (glShadowProgram != -1)
		{
			gl.glDeleteProgram(glShadowProgram);
			glShadowProgram = -1;
		}
	}

	public void recompilePrograms()
	{
		clientThread.invoke(() ->
			ThreadUtils.invokeOnMainThread(() ->
			{
				try
				{
					shutdownPrograms();
					shutdownVao();
					initVao();
					initPrograms();
				}
				catch (ShaderException ex)
				{
					log.error("Failed to recompile shader program", ex);
					stopPlugin();
				}
			})
		);
	}

	private void initVao()
	{
		// Create VAO
		vaoHandle = glGenVertexArrays(gl);

		// Create UI VAO
		vaoUiHandle = glGenVertexArrays(gl);
		// Create UI buffer
		vboUiHandle = glGenBuffers(gl);
		gl.glBindVertexArray(vaoUiHandle);

		FloatBuffer vboUiBuf = GpuFloatBuffer.allocateDirect(5 * 4);
		vboUiBuf.put(new float[]{
			// positions     // texture coords
			1f, 1f, 0.0f, 1.0f, 0f, // top right
			1f, -1f, 0.0f, 1.0f, 1f, // bottom right
			-1f, -1f, 0.0f, 0.0f, 1f, // bottom left
			-1f, 1f, 0.0f, 0.0f, 0f  // top left
		});
		vboUiBuf.rewind();
		gl.glBindBuffer(GL_ARRAY_BUFFER, vboUiHandle);
		gl.glBufferData(GL_ARRAY_BUFFER, vboUiBuf.capacity() * Float.BYTES, vboUiBuf, gl.GL_STATIC_DRAW);

		// position attribute
		gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, false, 5 * Float.BYTES, 0);
		gl.glEnableVertexAttribArray(0);

		// texture coord attribute
		gl.glVertexAttribPointer(1, 2, gl.GL_FLOAT, false, 5 * Float.BYTES, 3 * Float.BYTES);
		gl.glEnableVertexAttribArray(1);

		// unbind VBO
		gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
	}

	private void shutdownVao()
	{
		if (vaoHandle != -1)
		{
			glDeleteVertexArrays(gl, vaoHandle);
			vaoHandle = -1;
		}

		if (vboUiHandle != -1)
		{
			glDeleteBuffer(gl, vboUiHandle);
			vboUiHandle = -1;
		}

		if (vaoUiHandle != -1)
		{
			glDeleteVertexArrays(gl, vaoUiHandle);
			vaoUiHandle = -1;
		}
	}

	private void initBuffers()
	{
		initGlBuffer(sceneVertexBuffer);
		initGlBuffer(sceneUvBuffer);
		initGlBuffer(sceneNormalBuffer);
		initGlBuffer(tmpVertexBuffer);
		initGlBuffer(tmpUvBuffer);
		initGlBuffer(tmpNormalBuffer);
		initGlBuffer(tmpModelBufferLarge);
		initGlBuffer(tmpModelBufferSmall);
		initGlBuffer(tmpModelBufferUnordered);
		initGlBuffer(tmpOutBuffer);
		initGlBuffer(tmpOutUvBuffer);
		initGlBuffer(tmpOutNormalBuffer);
	}

	private void initGlBuffer(GLBuffer glBuffer)
	{
		glBuffer.glBufferId = glGenBuffers(gl);
	}

	private void shutdownBuffers()
	{
		destroyGlBuffer(sceneVertexBuffer);
		destroyGlBuffer(sceneUvBuffer);
		destroyGlBuffer(sceneNormalBuffer);

		destroyGlBuffer(tmpVertexBuffer);
		destroyGlBuffer(tmpUvBuffer);
		destroyGlBuffer(tmpNormalBuffer);
		destroyGlBuffer(tmpModelBufferLarge);
		destroyGlBuffer(tmpModelBufferSmall);
		destroyGlBuffer(tmpModelBufferUnordered);
		destroyGlBuffer(tmpOutBuffer);
		destroyGlBuffer(tmpOutUvBuffer);
		destroyGlBuffer(tmpOutNormalBuffer);
	}

	private void destroyGlBuffer(GLBuffer glBuffer)
	{
		if (glBuffer.glBufferId != -1)
		{
