package ru.dbotthepony.mc.otm.client.render

import com.mojang.blaze3d.pipeline.MainTarget
import com.mojang.blaze3d.platform.GlConst
import com.mojang.blaze3d.platform.GlStateManager
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.*
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.FogRenderer
import net.minecraft.client.renderer.GameRenderer
import net.minecraft.world.level.levelgen.XoroshiroRandomSource
import net.minecraft.world.level.material.FogType
import org.joml.Matrix4f
import ru.dbotthepony.mc.otm.capability.MatteryPlayerCapability
import ru.dbotthepony.mc.otm.capability.matteryPlayer
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.core.math.linearInterpolation
import ru.dbotthepony.mc.otm.milliTime
import java.lang.ref.WeakReference
import kotlin.math.absoluteValue

@Suppress("SameParameterValue")
object GlitchRenderer {
	private val random = XoroshiroRandomSource(System.nanoTime(), System.currentTimeMillis())

	var redShiftX = 0.0
		private set
	var redShiftY = 0.0
		private set
	var greenShiftX = 0.0
		private set
	var greenShiftY = 0.0
		private set
	var blueShiftX = 0.0
		private set
	var blueShiftY = 0.0
		private set

	var lastGlitch = System.nanoTime()
		private set

	var nextGlitch = 0L
		private set

	private val glitchBuffer by lazy(LazyThreadSafetyMode.NONE) {
		MainTarget(minecraft.window.width, minecraft.window.height)
	}

	private fun upload(builder: BufferBuilder, offsetX: Double, offsetY: Double, u0: Float, v0: Float, u1: Float, v1: Float) {
		builder.vertex(offsetX - 1.0, offsetY + 1.0, 0.0).uv(u0, v1).endVertex()
		builder.vertex(offsetX + 1.0, offsetY + 1.0, 0.0).uv(u1, v1).endVertex()
		builder.vertex(offsetX + 1.0, offsetY - 1.0, 0.0).uv(u1, v0).endVertex()
		builder.vertex(offsetX - 1.0, offsetY - 1.0, 0.0).uv(u0, v0).endVertex()
	}

	private fun uploadEmpty(builder: BufferBuilder, offsetX: Double, offsetY: Double) {
		builder.vertex(offsetX - 1.0, offsetY + 1.0, 0.0).endVertex()
		builder.vertex(offsetX + 1.0, offsetY + 1.0, 0.0).endVertex()
		builder.vertex(offsetX + 1.0, offsetY - 1.0, 0.0).endVertex()
		builder.vertex(offsetX - 1.0, offsetY - 1.0, 0.0).endVertex()
	}

	private fun uploadLine(builder: BufferBuilder, offsetX: Double, offsetY: Double, height: Double, u0: Float, v0: Float, u1: Float, v1: Float) {
		builder.vertex(offsetX - 1.0, offsetY - height, 0.0).uv(u0, v1).endVertex()
		builder.vertex(offsetX + 1.0, offsetY - height, 0.0).uv(u1, v1).endVertex()
		builder.vertex(offsetX + 1.0, offsetY, 0.0).uv(u1, v0).endVertex()
		builder.vertex(offsetX - 1.0, offsetY, 0.0).uv(u0, v0).endVertex()
	}

	private inline fun makeMirrors(handler: (x: Double, y: Double, u0: Float, v0: Float, u1: Float, v1: Float) -> Unit) {
		handler(0.0 - 2f, 0.0, 1f, 0f, 0f, 1f)
		handler(0.0 + 2f, 0.0, 1f, 0f, 0f, 1f)

		handler(0.0, 2.0, 0f, 1f, 1f, 0f)
		handler(0.0, -2.0, 0f, 1f, 1f, 0f)

		handler(0.0 - 2f, -2.0, 1f, 1f, 0f, 0f)
		handler(0.0 + 2f, -2.0, 1f, 1f, 0f, 0f)

		handler(0.0 - 2f, +2.0, 1f, 1f, 0f, 0f)
		handler(0.0 + 2f, +2.0, 1f, 1f, 0f, 0f)
	}

	private fun draw(offsetX: Double, offsetY: Double) {
		val builder = tesselator.builder

		builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX)

		upload(builder, offsetX, offsetY, 0f, 0f, 1f, 1f)

		if (offsetX != 0.0 || offsetY != 0.0) {
			makeMirrors { x: Double, y: Double, u0: Float, v0: Float, u1: Float, v1: Float ->
				upload(builder, offsetX + x, offsetY + y, u0, v0, u1, v1)
			}
		}

		BufferUploader.drawWithShader(builder.end())
	}

	private fun pixel2ViewX(value: Double) = linearInterpolation(value / glitchBuffer.width, 1.0, -1.0)
	private fun pixel2ViewY(value: Double) = linearInterpolation(value / glitchBuffer.height, 1.0, -1.0)
	private fun pixel2TextureX(value: Double) = (value / glitchBuffer.width).toFloat()
	private fun pixel2TextureY(value: Double) = 1f - (value / glitchBuffer.height).toFloat()

	private fun drawVHSLineGap(y: Double, height: Double) {
		val builder = tesselator.builder

		val v = pixel2TextureY(y)

		builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX)
		uploadLine(builder, 0.0, pixel2ViewY(y), (height / glitchBuffer.height) * 2.0, 0f, v, 1f, v)
		BufferUploader.drawWithShader(builder.end())
	}

	private fun makeGlitch() {
		redShiftX = random.nextDouble() * 0.05 - 0.025
		redShiftY = random.nextDouble() * 0.05 - 0.025
		greenShiftX = random.nextDouble() * 0.05 - 0.025
		greenShiftY = random.nextDouble() * 0.05 - 0.025
		blueShiftX = random.nextDouble() * 0.05 - 0.025
		blueShiftY = random.nextDouble() * 0.05 - 0.025
		lastGlitch = System.nanoTime()
		nextGlitch = random.nextIntBetweenInclusive(75_000_000, 400_000_000).toLong()
	}

	var glitchUntil = 0L
		private set
	var glitchSince = System.nanoTime()
		private set

	fun glitchFor(millis: Long) {
		if (glitchUntil - (System.nanoTime() - glitchSince) < millis) {
			glitchSince = System.nanoTime()
			glitchUntil = millis * 1_000_000L
		}
	}

	private var lastAndroid: WeakReference<MatteryPlayerCapability>? = null

	@JvmStatic
	fun render() {
		if (System.nanoTime() - glitchSince >= glitchUntil && ((minecraft.player?.matteryPlayer ?: lastAndroid?.get())?.isAndroid != true || minecraft.player?.isAlive != false)) {
			return
		}

		if (lastAndroid?.get() !== minecraft.player?.matteryPlayer && minecraft.player?.matteryPlayer != null)
			lastAndroid = WeakReference(minecraft.player?.matteryPlayer ?: throw ConcurrentModificationException())

		val glitchBuffer = glitchBuffer
		val projection = RenderSystem.getProjectionMatrix()
		RenderSystem.setProjectionMatrix(Matrix4f(), VertexSorting.ORTHOGRAPHIC_Z)

		RenderSystem.getModelViewStack().also {
			it.pushPose()
			it.setIdentity()
		}

		RenderSystem.applyModelViewMatrix()

		RenderSystem.disableCull()
		RenderSystem.disableDepthTest()
		RenderSystem.enableBlend()

		if (glitchBuffer.width != minecraft.window.width || glitchBuffer.height != minecraft.window.height) {
			glitchBuffer.resize(minecraft.window.width, minecraft.window.height, Minecraft.ON_OSX)
		}

		if (System.nanoTime() - lastGlitch >= nextGlitch) {
			makeGlitch()
		}

		if (!Minecraft.useShaderTransparency() && minecraft.gameRenderer.mainCamera.fluidInCamera == FogType.NONE) {
			glitchBuffer.bindWrite(true)
			RenderSystem.clearColor(FogRenderer.fogRed, FogRenderer.fogGreen, FogRenderer.fogBlue, 1f)
			RenderSystem.clear(GlConst.GL_COLOR_BUFFER_BIT, Minecraft.ON_OSX)

			// fix main rendertarget transparency (which is everything on screen that is not covered by anything, unless running Fabulous! graphics option)
			// this is required because Minecraft's builtin fragement shaders are stupid
			// and discard fully transparent fragments
			// when, in fact, alpha channel is just another color channel, and it's meaning is dictated
			// by render context, and it is not strictly "transparency"
			// the downside of this is RGBA8888 texture double copy, requiring a lot of video memory bandwidth
			// and no, trying to render "fog" solid color overlay quad over main rendertarget with proper blend mode
			RenderSystem.setShaderTexture(0, minecraft.mainRenderTarget.colorTextureId)
			RenderSystem.blendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ZERO, GlStateManager.DestFactor.ONE)
			RenderSystem.setShader(GameRenderer::getPositionTexShader)

			RenderSystem.setShaderColor(1f, 1f, 1f, 1f)
			draw(0.0, 0.0)
			minecraft.mainRenderTarget.bindWrite(true)

			RenderSystem.setShader(GameRenderer::getPositionTexShader)

			RenderSystem.blendFunc(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO)
			RenderSystem.setShaderColor(1f, 1f, 1f, 1f)
			RenderSystem.setShaderTexture(0, glitchBuffer.colorTextureId)
			draw(0.0, 0.0)
		}

		glitchBuffer.bindWrite(true)
		RenderSystem.clearColor(0f, 0f, 0f, 1f)
		RenderSystem.clear(GlConst.GL_COLOR_BUFFER_BIT, Minecraft.ON_OSX)

		// distort colors by sampling main frame buffer (raw stage)
		RenderSystem.setShaderTexture(0, minecraft.mainRenderTarget.colorTextureId)
		RenderSystem.blendFuncSeparate(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ONE, GlStateManager.SourceFactor.ZERO, GlStateManager.DestFactor.ONE)

		RenderSystem.setShader(GameRenderer::getPositionTexShader)

		RenderSystem.setShaderColor(1f, 0.1f, 0f, 1f)
		draw(redShiftX, redShiftY)

		RenderSystem.setShaderColor(0f, 1f, 0f, 1f)
		draw(greenShiftX, greenShiftY)

		RenderSystem.setShaderColor(0f, 0f, 1f, 1f)
		draw(blueShiftX, blueShiftY)

		minecraft.mainRenderTarget.bindWrite(true)

		// draw back to main frame buffer (prepare for post process)
		RenderSystem.blendFunc(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO)
		RenderSystem.setShaderColor(1f, 1f, 1f, 1f)
		RenderSystem.setShaderTexture(0, glitchBuffer.colorTextureId)
		draw(0.0, 0.0)

		// return to our frame buffer (post process stage)
		glitchBuffer.bindWrite(true)
		RenderSystem.setShaderTexture(0, minecraft.mainRenderTarget.colorTextureId)

		// color perception errors (eye-camera glitch)
		drawVHSLineGap((milliTime % glitchBuffer.height).toDouble(), glitchBuffer.height * 0.025)
		drawVHSLineGap(((milliTime + glitchBuffer.height / 2) % glitchBuffer.height).toDouble(), glitchBuffer.height * 0.075)
		drawVHSLineGap(((milliTime + glitchBuffer.height / 3) % glitchBuffer.height).toDouble(), glitchBuffer.height * 0.04)
		drawVHSLineGap(((-milliTime - glitchBuffer.height / 3) % glitchBuffer.height).toDouble().absoluteValue, glitchBuffer.height * 0.07)

		// upload final result to main frame buffer
		minecraft.mainRenderTarget.bindWrite(true)

		RenderSystem.setShader(GameRenderer::getPositionTexShader)

		RenderSystem.blendFunc(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO)
		RenderSystem.setShaderColor(1f, 1f, 1f, 1f)
		RenderSystem.setShaderTexture(0, glitchBuffer.colorTextureId)
		draw(0.0, 0.0)

		RenderSystem.setProjectionMatrix(projection, VertexSorting.DISTANCE_TO_ORIGIN)
		RenderSystem.getModelViewStack().popPose()
		RenderSystem.applyModelViewMatrix()
	}
}
