package ru.dbotthepony.mc.otm.client

import com.mojang.blaze3d.platform.InputConstants
import com.mojang.blaze3d.systems.RenderSystem
import it.unimi.dsi.fastutil.objects.Object2FloatArrayMap
import it.unimi.dsi.fastutil.objects.Object2FloatFunction
import net.minecraft.client.KeyMapping
import net.minecraftforge.client.event.InputEvent
import net.minecraftforge.client.event.RegisterKeyMappingsEvent
import net.minecraftforge.client.event.RenderGuiEvent
import net.minecraftforge.client.settings.KeyConflictContext
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.android.AndroidFeature
import ru.dbotthepony.mc.otm.android.AndroidSwitchableFeature
import ru.dbotthepony.mc.otm.capability.matteryPlayer
import ru.dbotthepony.mc.otm.client.render.RenderGravity
import ru.dbotthepony.mc.otm.client.render.drawAligned
import ru.dbotthepony.mc.otm.client.render.drawArc
import ru.dbotthepony.mc.otm.core.math.RGBAColor
import ru.dbotthepony.mc.otm.core.math.angleDifference
import ru.dbotthepony.mc.otm.core.math.normalizeAngle
import ru.dbotthepony.mc.otm.core.util.formatTickDuration
import ru.dbotthepony.mc.otm.milliTimeD
import ru.dbotthepony.mc.otm.network.MatteryPlayerNetworkChannel
import ru.dbotthepony.mc.otm.network.SwitchAndroidFeaturePacket
import java.util.stream.Collectors
import kotlin.math.PI
import kotlin.math.acos
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin

// TODO: forge's KeyMapping patch (ALL and MAP maps) is not thread safe... should we be worried?
object AndroidMenuKeyMapping : KeyMapping("key.otm.android_menu", KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM.getOrCreate(InputConstants.KEY_F), OverdriveThatMatters.MOD_ID) {
	private var grabbedInput = false
	private var selectedFeature: AndroidSwitchableFeature? = null
	private var lastSelectedFeature: AndroidSwitchableFeature? = null

	private var lastRender = milliTimeD
	private var lastSelectedDegree: Double? = null
	private val lastSelectProgress = Object2FloatArrayMap<AndroidFeature>()
	private var lastSelectProgressGlobal = 0f

	private var holdIt: AndroidSwitchableFeature? = null

	private var clickOnce = false

	private fun resetState() {
		selectedFeature = null
		lastSelectedFeature = null
		lastSelectedDegree = null
		lastSelectProgress.clear()
		clickOnce = false
		holdIt = null
	}

	private fun ohNoIHaveHitSomethingNastyInMinecraftCode() {
		grabbedInput = false
		resetState()
	}

	override fun setDown(isDown: Boolean) {
		val old = this.isDown
		super.setDown(isDown)

		if (old != isDown) {
			val player = minecraft.player
			val matteryPlayer = player?.matteryPlayer

			if (
				minecraft.screen == null &&
				isDown &&
				matteryPlayer?.isAndroid == true &&
				matteryPlayer.features.anyMatch { it is AndroidSwitchableFeature && it.allowToSwitchByPlayer && (!player.isSpectator || it.allowToSwitchByPlayerWhileSpectator) }
			) {
				grabbedInput = true
				minecraft.mouseHandler.releaseMouse()
			} else if (!isDown && grabbedInput) {
				grabbedInput = false
				minecraft.mouseHandler.grabMouse()

				if (minecraft.screen == null) {
					if (!clickOnce) {
						val selectedFeature = selectedFeature

						if (selectedFeature != null) {
							MatteryPlayerNetworkChannel.sendToServer(SwitchAndroidFeaturePacket(selectedFeature.type, !selectedFeature.isActive))
							playGuiClickSound()
						}
					}
				}
			}

			resetState()
		}
	}

	fun onMouseClick(event: InputEvent.MouseButton.Pre) {
		// fix binding wheel menu to mouse button not calling back setIsDown(false)
		if (isDown && event.action == 0 && key.type == InputConstants.Type.MOUSE && InputConstants.Type.MOUSE.getOrCreate(event.button) == key) {
			isDown = false
			return
		}

		if (grabbedInput) {
			if (minecraft.screen != null) {
				// whoopsie
				ohNoIHaveHitSomethingNastyInMinecraftCode()
				return
			}

			clickOnce = true
			event.isCanceled = true

			if (event.button == InputConstants.MOUSE_BUTTON_LEFT && event.action == 1) {
				if (selectedFeature != null) {
					playGuiClickSound()
					holdIt = selectedFeature
				}
			} else if (event.button == InputConstants.MOUSE_BUTTON_LEFT && event.action == 0) {
				holdIt = null
				val selectedFeature = selectedFeature

				if (selectedFeature != null) {
					MatteryPlayerNetworkChannel.sendToServer(SwitchAndroidFeaturePacket(selectedFeature.type, !selectedFeature.isActive))
				}
			}
		}
	}

	private fun renderWheel(event: RenderGuiEvent.Post) {
		val player = minecraft.player ?: return
		val matteryPlayer = player.matteryPlayer ?: return

		if (!matteryPlayer.isAndroid) {
			return
		}

		val features = matteryPlayer.features
			.filter { it is AndroidSwitchableFeature && it.allowToSwitchByPlayer && (!player.isSpectator || it.allowToSwitchByPlayerWhileSpectator) }
			.collect(Collectors.toList()) as MutableList<AndroidSwitchableFeature>

		if (features.isEmpty()) {
			lastRender = milliTimeD
			return
		}

		features.sortWith { a, b ->
			return@sortWith a.type.registryName!!.compareTo(b.type.registryName!!)
		}

		val delta = milliTimeD - lastRender
		lastRender = milliTimeD

		RenderSystem.setShaderColor(0f, 0f, 0f, 0.6f)

		val size = minecraft.window.guiScaledHeight.coerceAtMost(minecraft.window.guiScaledWidth).toFloat() * 0.35f

		drawArc(
			event.guiGraphics,
			minecraft.window.guiScaledWidth / 2f,
			minecraft.window.guiScaledHeight / 2f,
			size,
			size * 0.3f
		)

		RenderSystem.setShaderColor(85 / 255f, 197 / 255f, 255 / 255f, 0.5f)

		val degreePerSlice = PI * 2.0 / features.size

		var (mouseX, mouseY) = mousePos

		mouseX -= minecraft.window.width / 2.0
		mouseY -= minecraft.window.height / 2.0

		val length = (mouseX * mouseX + mouseY * mouseY).pow(0.5)

		if ((length / minecraft.window.guiScale) in (size * 0.3f) .. size || holdIt != null) {
			var deg = acos(mouseX / length)

			// opengl
			// lower values are at bottom of screen
			if (mouseY > 0.0) {
				deg = -deg
			}

			if (deg < 0.0) {
				deg += PI * 2
			}

			val index = if (holdIt != null) features.indexOf(holdIt) else (deg / degreePerSlice).toInt()
			selectedFeature = features[index]
			lastSelectedFeature = features[index]

			lastSelectProgress.computeIfAbsent(features[index], Object2FloatFunction { 0f })

			RenderSystem.setShaderColor(85 / 255f, 197 / 255f, 255 / 255f, 0.3f * lastSelectProgress.getFloat(features[index]))
			lastSelectProgressGlobal = (lastSelectProgressGlobal + delta * 0.01).coerceAtMost(1.0).toFloat()

			val lastSelectedDegree = lastSelectedDegree

			if (lastSelectedDegree == null) {
				this.lastSelectedDegree = normalizeAngle(index * degreePerSlice)
			} else {
				this.lastSelectedDegree = lastSelectedDegree + angleDifference(index * degreePerSlice, lastSelectedDegree) * delta * 0.02
			}
		} else {
			lastSelectProgressGlobal = (lastSelectProgressGlobal - delta * 0.01).coerceAtLeast(0.0).toFloat()

			selectedFeature = null

			if (lastSelectProgress.values.all { it <= 0f }) {
				lastSelectedDegree = null
				lastSelectedFeature = null
			}
		}

		for (key in lastSelectProgress.keys) {
			if (key == selectedFeature) {
				lastSelectProgress[key] = (lastSelectProgress.getFloat(key) + delta * 0.01).coerceAtMost(1.0).toFloat()
			} else {
				lastSelectProgress[key] = (lastSelectProgress.getFloat(key) - delta * 0.01).coerceAtLeast(0.0).toFloat()
			}
		}

		if (lastSelectedDegree != null && lastSelectedFeature != null) {
			RenderSystem.setShaderColor(85 / 255f, 197 / 255f, 255 / 255f, 0.3f * lastSelectProgressGlobal)

			drawArc(
				event.guiGraphics,
				minecraft.window.guiScaledWidth / 2f,
				minecraft.window.guiScaledHeight / 2f,
				ru.dbotthepony.mc.otm.core.math.linearInterpolation(lastSelectProgressGlobal, size, size * 1.2f),
				ru.dbotthepony.mc.otm.core.math.linearInterpolation(lastSelectProgressGlobal, size * 0.3f, size * 0.4f),

				startDegree = lastSelectedDegree!!,
				endDegree = lastSelectedDegree!! + degreePerSlice
			)
		}

		RenderSystem.setShaderColor(1f, 1f, 1f, 1f)

		val iconSize = size * 0.25f

		val poseStack = event.guiGraphics.pose()
		poseStack.pushPose()
		poseStack.translate(minecraft.window.guiScaledWidth.toDouble() / 2f, minecraft.window.guiScaledHeight.toDouble() / 2f, 0.0)

		for ((index, feature) in features.withIndex()) {
			var sin = sin((index + 0.5) * degreePerSlice).toFloat()
			var cos = cos((index + 0.5) * degreePerSlice).toFloat()

			if (features.size == 1) {
				sin = sin(PI / 2.0).toFloat()
				cos = cos(PI / 2.0).toFloat()
			}

			val shift = size * 0.6f
			feature.renderIcon(event.guiGraphics, -iconSize / 2f + shift * cos, -shift * sin - iconSize / 2f, iconSize, iconSize)
			event.guiGraphics.drawAligned(minecraft.font, feature.type.displayName, RenderGravity.CENTER_CENTER, shift * cos + 1f, -shift * sin - iconSize / 1.5f + 1f, 0)
			event.guiGraphics.drawAligned(minecraft.font, feature.type.displayName, RenderGravity.CENTER_CENTER, shift * cos, -shift * sin - iconSize / 1.5f, if (feature.isActive) RGBAColor.DARK_GREEN else RGBAColor.DARK_RED)

			if (feature.isOnCooldown && feature.cooldownPercent > 0.0f) {
				RenderSystem.setShaderColor(1f, 1f, 1f, 0.5f)
				drawArc(event.guiGraphics, shift * cos,  -shift * sin, iconSize / 2f, 0f, PI / 2.0, PI / 2.0 + PI * 2.0 * feature.cooldownPercent.toDouble())
				RenderSystem.setShaderColor(1f, 1f, 1f, 1f)

				event.guiGraphics.drawAligned(minecraft.font, formatTickDuration(feature.cooldown), RenderGravity.CENTER_CENTER, shift * cos, -shift * sin + iconSize / 1.5f, RGBAColor.WHITE)
			}
		}

		poseStack.popPose()
	}

	private fun renderRegular(event: RenderGuiEvent.Post) {
		val player = minecraft.player ?: return
		val matteryPlayer = player.matteryPlayer ?: return

		if (!matteryPlayer.isAndroid) {
			return
		}

		val features = matteryPlayer.features
			.filter { it is AndroidSwitchableFeature && it.isOnCooldown }
			.collect(Collectors.toList()) as MutableList<AndroidSwitchableFeature>

		if (features.isEmpty()) {
			return
		}

		features.sortWith { a, b ->
			return@sortWith a.type.registryName!!.compareTo(b.type.registryName!!)
		}

		val y = minecraft.window.guiScaledHeight * 0.2f
		var x = minecraft.window.guiScaledWidth * 0.5f - (features.size.toFloat() * COOLDOWN_ICON_SIZE / 2f + (features.size - 1).toFloat() * (COOLDOWN_ICON_MARGIN / 2f))

		for (feature in features) {
			feature.renderIcon(event.guiGraphics, x, y, COOLDOWN_ICON_SIZE, COOLDOWN_ICON_SIZE)

			RenderSystem.setShaderColor(1f, 1f, 1f, 0.5f)
			drawArc(event.guiGraphics, x, y, COOLDOWN_ICON_SIZE / 2f, 0f, PI / 2.0, PI / 2.0 + PI * 2.0 * feature.cooldownPercent, alignAtCenter = false)
			RenderSystem.setShaderColor(1f, 1f, 1f, 1f)

			event.guiGraphics.drawAligned(font, formatTickDuration(feature.cooldown), RenderGravity.TOP_CENTER, x + COOLDOWN_ICON_SIZE / 2f, y + COOLDOWN_ICON_SIZE + 1f, RGBAColor.WHITE)

			x += COOLDOWN_ICON_SIZE + COOLDOWN_ICON_MARGIN
		}
	}

	const val COOLDOWN_ICON_SIZE = 18f
	const val COOLDOWN_ICON_MARGIN = 12f

	fun onRenderGuiEvent(event: RenderGuiEvent.Post) {
		if (!grabbedInput) {
			lastRender = milliTimeD
			renderRegular(event)
			return
		} else {
			renderWheel(event)

			if (minecraft.screen != null) {
				ohNoIHaveHitSomethingNastyInMinecraftCode()
			}
		}
	}

	fun register(event: RegisterKeyMappingsEvent) {
		event.register(this)
	}
}
