package ru.dbotthepony.kstarbound.util

import org.apache.logging.log4j.LogManager
import org.lwjgl.system.MemoryStack
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.WindowsBindings
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport
import java.util.function.BooleanSupplier

class ExecutionSpinner(private val waiter: Runnable, private val spinner: BooleanSupplier, private val timeBetweenFrames: Long) : Runnable {
	init {
		Companion
	}

	private var lastRender = System.nanoTime()
	private var frameRenderTime = 0L
	private val frameRenderTimes = LongArray(60) { 1L }
	private var frameRenderIndex = 0
	private val renderWaitTimes = LongArray(60) { 1L }
	private var renderWaitIndex = 0

	val averageRenderWait: Double get() {
		var sum = 0.0

		for (value in renderWaitTimes)
			sum += value

		if (sum == 0.0)
			return 0.0

		sum /= 1_000_000_000.0
		return sum / renderWaitTimes.size
	}

	val averageRenderTime: Double get() {
		var sum = 0.0

		for (value in frameRenderTimes)
			sum += value

		if (sum == 0.0)
			return 0.0

		sum /= 1_000_000_000.0
		return sum / frameRenderTimes.size
	}

	private fun timeUntilNextFrame(): Long {
		return Starbound.TIMESTEP_NANOS - (System.nanoTime() - lastRender) - frameRenderTime - compensate
	}

	private var compensate = 0L

	private var carrier: Thread? = null
	private val pause = AtomicInteger()

	fun pause() {
		pause.incrementAndGet()
	}

	fun unpause() {
		if (pause.addAndGet(-100) <= 0) {
			carrier?.let { LockSupport.unpark(it) }
		}
	}

	fun spin(): Boolean {
		carrier = Thread.currentThread()

		while (pause.get() > 0) {
			waiter.run()
			LockSupport.park()
		}

		var diff = timeUntilNextFrame()

		while (diff > 0L) {
			waiter.run()
			diff = timeUntilNextFrame()
			LockSupport.parkNanos(diff)
		}

		compensate = -diff

		val mark = System.nanoTime()
		val result = spinner.asBoolean
		frameRenderTime = System.nanoTime() - mark
		frameRenderTimes[++frameRenderIndex % frameRenderTimes.size] = frameRenderTime
		renderWaitTimes[++renderWaitIndex % renderWaitTimes.size] = System.nanoTime() - lastRender
		lastRender = System.nanoTime()

		return result
	}

	override fun run() {
		while (spin()) {}
	}

	companion object {
		private val LOGGER = LogManager.getLogger()
		private var SYSTEM_SCHEDULER_RESOLUTION = 1_000_000L

		init {
			val bindings = WindowsBindings.INSTANCE

			if (bindings != null) {
				MemoryStack.stackPush().use { stack ->
					val minimum = stack.mallocLong(1)
					val maximum = stack.mallocLong(1)
					val current = stack.mallocLong(1)

					minimum.put(0L)
					maximum.put(0L)
					current.put(0L)

					minimum.position(0)
					maximum.position(0)
					current.position(0)

					bindings.NtQueryTimerResolution(minimum, maximum, current)

					SYSTEM_SCHEDULER_RESOLUTION = current[0] * 100L
					LOGGER.info("NtQueryTimerResolution(): {} ns/{} ns/{} ns min/max/current", minimum[0] * 100L, maximum[0] * 100L, current[0] * 100L)
				}
			}

			val thread = object : Thread("Process scheduler timer hack thread") {
				override fun run() {
					while (true) {
						try {
							sleep(Int.MAX_VALUE.toLong())
						} catch (err: InterruptedException) {
							LOGGER.error("Timer hack thread was interrupted, ignoring.")
						}
					}
				}
			}

			thread.isDaemon = true
			thread.start()
		}
	}
}
