package ru.dbotthepony.kstarbound.client

import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import io.netty.channel.Channel
import io.netty.channel.local.LocalAddress
import org.apache.logging.log4j.LogManager
import org.lwjgl.BufferUtils
import org.lwjgl.glfw.Callbacks
import org.lwjgl.glfw.GLFW
import org.lwjgl.glfw.GLFWErrorCallback
import org.lwjgl.glfw.GLFWImage
import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL45.*
import org.lwjgl.opengl.GLCapabilities
import org.lwjgl.stb.STBImage
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil
import org.lwjgl.system.MemoryUtil.memAddressSafe
import ru.dbotthepony.kommons.collect.forValidRefs
import ru.dbotthepony.kommons.util.IStruct4f
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.matrix.Matrix3f
import ru.dbotthepony.kommons.matrix.Matrix3fStack
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kommons.vector.Vector4f
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.freetype.FreeType
import ru.dbotthepony.kstarbound.client.freetype.InvalidArgumentException
import ru.dbotthepony.kstarbound.client.gl.BlendFunc
import ru.dbotthepony.kstarbound.client.gl.GLFrameBuffer
import ru.dbotthepony.kstarbound.client.gl.GLTexture2D
import ru.dbotthepony.kstarbound.client.gl.ScissorRect
import ru.dbotthepony.kstarbound.client.gl.VertexArrayObject
import ru.dbotthepony.kstarbound.client.gl.BufferObject
import ru.dbotthepony.kstarbound.client.gl.checkForGLError
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateFuncTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLGenericTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLObjectTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateIntTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateSwitchTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLTexturesTracker
import ru.dbotthepony.kstarbound.client.gl.shader.FontProgram
import ru.dbotthepony.kstarbound.client.gl.shader.GLPrograms
import ru.dbotthepony.kstarbound.client.gl.shader.GLShader
import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram
import ru.dbotthepony.kstarbound.client.gl.shader.UberShader
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder
import ru.dbotthepony.kstarbound.client.input.UserInput
import ru.dbotthepony.kstarbound.client.render.Camera
import ru.dbotthepony.kstarbound.client.render.Font
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.TileRenderers
import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.image.Image
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.RayDirection
import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import java.io.Closeable
import java.io.File
import java.lang.ref.PhantomReference
import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference
import java.net.SocketAddress
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.time.Duration
import java.util.*
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinWorkerThread
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Consumer
import java.util.function.IntConsumer
import kotlin.collections.ArrayList
import kotlin.concurrent.withLock
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
import kotlin.properties.Delegates

class StarboundClient private constructor(val clientID: Int) : BlockableEventLoop("Client Thread $clientID"), Closeable {
	constructor() : this(COUNTER.getAndIncrement())

	var window: Long = 0L
		private set
	val camera = Camera(this)
	val input = UserInput()
	val thread: Thread = this
	private val threadCounter = AtomicInteger()

	// client specific executor which will accept tasks which involve probable
	// callback to foreground executor to initialize thread-unsafe data
	// In above case too many threads will introduce big congestion for resources, stalling entire workload; wasting cpu resources
	val executor = ForkJoinPool(Runtime.getRuntime().availableProcessors().coerceAtMost(4), {
		object : ForkJoinWorkerThread(it) {
			init {
				name = "Starbound Client $clientID executor ${threadCounter.incrementAndGet()}"
			}

			override fun onTermination(exception: Throwable?) {
				super.onTermination(exception)

				if (exception != null) {
					LOGGER.error("$this encountered an exception while executing task", exception)
				}
			}
		}
	}, null, false)

	@Deprecated("Use this directly", replaceWith = ReplaceWith("this"))
	val mailbox = this
	var capabilities: GLCapabilities by Delegates.notNull()
		private set

	var viewportX: Int = 0
		private set
	var viewportY: Int = 0
		private set
	var viewportWidth: Int = 0
		private set
	var viewportHeight: Int = 0
		private set

	// potentially visible cells
	var viewportCellX = 0
		private set
	var viewportCellY = 0
		private set
	var viewportCellWidth = 0
		private set
	var viewportCellHeight = 0
		private set
	var viewportRectangle = AABB.rectangle(Vector2d.ZERO, 0.0, 0.0)
		private set

	var viewportBottomLeft = Vector2d()
		private set
	var viewportTopRight = Vector2d()
		private set

	var fullbright = true

	var viewportMatrixScreen: Matrix3f = Matrix3f.rowMajor()
		private set
		get() = Matrix3f.unmodifiable(field)

	var viewportMatrixWorld: Matrix3f = Matrix3f.rowMajor()
		private set
		get() = Matrix3f.unmodifiable(field)

	var isRenderingGame = true
		private set

	var activeConnection: ClientConnection? = null
		private set

	fun connectToLocalServer(client: StarboundClient, address: LocalAddress) {
		check(activeConnection == null) { "Already having active connection to server: $activeConnection" }
		activeConnection = ClientConnection.connectToLocalServer(client, address)
	}

	fun connectToLocalServer(address: Channel) {
		check(activeConnection == null) { "Already having active connection to server: $activeConnection" }
		activeConnection = ClientConnection.connectToLocalServer(this, address)
	}

	fun connectToRemoteServer(address: SocketAddress) {
		check(activeConnection == null) { "Already having active connection to server: $activeConnection" }
		activeConnection = ClientConnection.connectToRemoteServer(this, address)
	}

	private val scissorStack = LinkedList<ScissorRect>()
	private val onDrawGUI = ArrayList<() -> Unit>()
	private val onViewportChanged = ArrayList<(width: Int, height: Int) -> Unit>()

	private val openglCleanQueue = ReferenceQueue<Any>()
	private var openglCleanQueueHead: CleanRef? = null

	private class CleanRef(ref: Any, queue: ReferenceQueue<Any>, val fn: IntConsumer, val value: Int) : PhantomReference<Any>(ref, queue) {
		var next: CleanRef? = null
		var prev: CleanRef? = null
	}

	var openglObjectsCreated = 0L
		private set

	var openglObjectsCleaned = 0L
		private set

	var maxTextureBlocks: Int = 0
		private set
	var maxUserTextureBlocks: Int = 0 // available textures blocks for generic use
		private set
	var maxVertexAttribBindPoints: Int = 0
		private set

	var lightMapLocation = maxTextureBlocks - 1
		private set

	init {
		CLIENTS.set(this)
	}

	val stack = Matrix3fStack()

	// минимальное время хранения 5 минут и...
	val named2DTextures0: Cache<Image, GLTexture2D> = Caffeine.newBuilder()
		.expireAfterAccess(Duration.ofMinutes(1))
		.scheduler(Starbound)
		.build()

	// ...бесконечное хранение пока кто-то все ещё использует текстуру
	val named2DTextures1: Cache<Image, GLTexture2D> = Caffeine.newBuilder()
		.weakValues()
		.weakKeys()
		.build()

	private val fontShaderPrograms = ArrayList<WeakReference<FontProgram>>()
	private val uberShaderPrograms = ArrayList<WeakReference<UberShader>>()

	fun addShaderProgram(program: GLShaderProgram) {
		if (program is UberShader) {
			uberShaderPrograms.add(WeakReference(program))
		}

		if (program is FontProgram) {
			fontShaderPrograms.add(WeakReference(program))
		}
	}

	fun registerCleanable(ref: Any, fn: IntConsumer, nativeRef: Int) {
		openglObjectsCreated++
		val ref0 = CleanRef(ref, openglCleanQueue, fn, nativeRef)

		if (openglCleanQueueHead == null) {
			openglCleanQueueHead = ref0
		} else {
			ref0.next = openglCleanQueueHead
			openglCleanQueueHead!!.prev = ref0
			openglCleanQueueHead = ref0
		}
	}

	private fun performOpenGLCleanup() {
		var next = openglCleanQueue.poll() as CleanRef?
		var i = 0

		while (next != null) {
			openglObjectsCleaned++
			next.fn.accept(next.value)
			checkForGLError("Removing unreachable OpenGL object")

			val head = openglCleanQueueHead

			if (next === head) {
				openglCleanQueueHead = head.next
			} else {
				next.prev?.next = next.next
				next.next?.prev = next.prev
			}

			if (++i > 400) break
			next = openglCleanQueue.poll() as CleanRef?
		}
	}

	var blend by GLStateSwitchTracker(GL_BLEND)
	var scissor by GLStateSwitchTracker(GL_SCISSOR_TEST)

	var cull by GLStateSwitchTracker(GL_CULL_FACE)
	var cullMode by GLStateFuncTracker(::glCullFace, GL_BACK)

	var textureUnpackAlignment by GLStateIntTracker(::glPixelStorei, GL_UNPACK_ALIGNMENT, 4)

	var scissorRect by GLGenericTracker(ScissorRect(0, 0, 0, 0)) {
		// require(it.x >= 0) { "Invalid X ${it.x}"}
		// require(it.y >= 0) { "Invalid Y ${it.y}"}

		require(it.width >= 0) { "Invalid width ${it.width}"}
		require(it.height >= 0) { "Invalid height ${it.height}"}

		glScissor(it.x, it.y, it.width, it.height)
	}

	var depthTest by GLStateSwitchTracker(GL_DEPTH_TEST)

	var vao by GLObjectTracker<VertexArrayObject>(::glBindVertexArray)
	var framebuffer by GLObjectTracker<GLFrameBuffer>(::glBindFramebuffer, GL_FRAMEBUFFER)
	var program by GLObjectTracker<GLShaderProgram>(::glUseProgram)

	var textures2D: GLTexturesTracker<GLTexture2D> by Delegates.notNull()
		private set

	var clearColor by GLGenericTracker<IStruct4f>(RGBAColor.WHITE) {
		val (r, g, b, a) = it
		glClearColor(r, g, b, a)
	}

	var blendFunc by GLGenericTracker(BlendFunc()) {
		glBlendFuncSeparate(it.sourceColor.enum, it.destinationColor.enum, it.sourceAlpha.enum, it.destinationAlpha.enum)
	}

	val freeType = FreeType()

	var font: Font by Delegates.notNull()
		private set

	private var fontInitialized = false

	val programs = GLPrograms()

	val whiteTexture by lazy(LazyThreadSafetyMode.NONE) {
		val texture = GLTexture2D(1, 1, GL_RGB8)

		val buffer = ByteBuffer.allocateDirect(3)
		buffer.put(0xFF.toByte())
		buffer.put(0xFF.toByte())
		buffer.put(0xFF.toByte())
		buffer.position(0)
		texture.upload(GL_RGB, GL_UNSIGNED_BYTE, buffer)

		texture
	}

	val missingTexture by lazy(LazyThreadSafetyMode.NONE) {
		val texture = GLTexture2D(8, 8, GL_RGB8)
		val buffer = ByteBuffer.allocateDirect(3 * 8 * 8)

		for (row in 0 until 4) {
			for (x in 0 until 4) {
				buffer.put(0x80.toByte())
				buffer.put(0x0.toByte())
				buffer.put(0x80.toByte())
			}

			for (x in 0 until 4) {
				buffer.put(0x0.toByte())
				buffer.put(0x0.toByte())
				buffer.put(0x0.toByte())
			}
		}

		for (row in 0 until 4) {
			for (x in 0 until 4) {
				buffer.put(0x0.toByte())
				buffer.put(0x0.toByte())
				buffer.put(0x0.toByte())
			}

			for (x in 0 until 4) {
				buffer.put(0x80.toByte())
				buffer.put(0x0.toByte())
				buffer.put(0x80.toByte())
			}
		}

		buffer.position(0)
		texture.upload(GL_RGB, GL_UNSIGNED_BYTE, buffer)
		texture
	}

	fun setViewport(x: Int, y: Int, width: Int, height: Int) {
		ensureSameThread()

		if (viewportX != x || viewportY != y || viewportWidth != width || viewportHeight != height) {
			glViewport(x, y, width, height)
			checkForGLError("Setting viewport")
			viewportX = x
			viewportY = y
			viewportWidth = width
			viewportHeight = height
		}
	}

	fun pushScissorRect(x: Float, y: Float, width: Float, height: Float) {
		return pushScissorRect(x.roundToInt(), y.roundToInt(), width.roundToInt(), height.roundToInt())
	}

	@Suppress("NAME_SHADOWING")
	fun pushScissorRect(x: Int, y: Int, width: Int, height: Int) {
		var x = x
		var y = y
		var width = width
		var height = height

		val peek = scissorStack.lastOrNull()

		if (peek != null) {
			x = x.coerceAtLeast(peek.x)
			y = y.coerceAtLeast(peek.y)
			width = width.coerceAtMost(peek.width)
			height = height.coerceAtMost(peek.height)

			if (peek.x == x && peek.y == y && peek.width == width && peek.height == height) {
				scissorStack.add(peek)
				return
			}
		}

		val rect = ScissorRect(x, y, width, height)
		scissorStack.add(rect)
		scissorRect = rect
		scissor = true
	}

	fun popScissorRect() {
		scissorStack.removeLast()

		val peek = scissorStack.lastOrNull()

		if (peek == null) {
			scissor = false
			return
		}

		val y = viewportHeight - peek.y - peek.height
		scissorRect = ScissorRect(peek.x, y, peek.width, peek.height)
	}

	val currentScissorRect get() = scissorStack.lastOrNull()

	fun newEBO() = BufferObject.EBO()
	fun newVBO() = BufferObject.VBO()
	fun newVAO() = VertexArrayObject()

	inline fun quadWireframe(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) {
		val builder = programs.position.builder

		builder.builder.begin(GeometryType.QUADS_AS_LINES_WIREFRAME)
		lambda.invoke(builder.builder)
		builder.upload()

		programs.position.use()
		programs.position.colorMultiplier = color
		programs.position.modelMatrix = stack.last()

		builder.draw(GL_LINES)
	}

	inline fun quadColor(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) {
		val builder = programs.position.builder

		builder.builder.begin(GeometryType.QUADS)
		lambda.invoke(builder.builder)
		builder.upload()

		programs.position.use()
		programs.position.colorMultiplier = color
		programs.position.modelMatrix = stack.last()

		builder.draw(GL_TRIANGLES)
	}

	inline fun lines(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) {
		val builder = programs.position.builder

		builder.builder.begin(GeometryType.LINES)
		lambda.invoke(builder.builder)
		builder.upload()

		programs.position.use()
		programs.position.colorMultiplier = color
		programs.position.modelMatrix = stack.last()

		builder.draw(GL_LINES)
	}

	fun vertex(file: File) = GLShader(file, GL_VERTEX_SHADER)
	fun fragment(file: File) = GLShader(file, GL_FRAGMENT_SHADER)

	fun vertex(contents: String) = GLShader(contents, GL_VERTEX_SHADER)
	fun fragment(contents: String) = GLShader(contents, GL_FRAGMENT_SHADER)

	fun internalVertex(file: String) = GLShader(readInternal(file), GL_VERTEX_SHADER)
	fun internalFragment(file: String) = GLShader(readInternal(file), GL_FRAGMENT_SHADER)
	fun internalGeometry(file: String) = GLShader(readInternal(file), GL_GEOMETRY_SHADER)

	private fun isMe(state: StarboundClient?) {
		if (state != null && state != this) {
			throw InvalidArgumentException("Provided object does not belong to $this state tracker (belongs to $state)")
		}
	}

	private fun updateViewportMatrixScreen(): Matrix3f {
		return Matrix3f.ortho(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat())
	}

	private fun updateViewportMatrixWorld(): Matrix3f {
		return Matrix3f.orthoDirect(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat())
	}

	private val xMousePos = ByteBuffer.allocateDirect(8).also { it.order(ByteOrder.LITTLE_ENDIAN) }.asDoubleBuffer()
	private val yMousePos = ByteBuffer.allocateDirect(8).also { it.order(ByteOrder.LITTLE_ENDIAN) }.asDoubleBuffer()

	val mouseCoordinates: Vector2d get() {
		xMousePos.position(0)
		yMousePos.position(0)
		GLFW.glfwGetCursorPos(window, xMousePos, yMousePos)
		xMousePos.position(0)
		yMousePos.position(0)

		return Vector2d(xMousePos.get(), viewportHeight - yMousePos.get())
	}

	val mouseCoordinatesF: Vector2f get() {
		xMousePos.position(0)
		yMousePos.position(0)
		GLFW.glfwGetCursorPos(window, xMousePos, yMousePos)
		xMousePos.position(0)
		yMousePos.position(0)

		return Vector2f(xMousePos.get().toFloat(), viewportHeight - yMousePos.get().toFloat())
	}

	fun screenToWorld(x: Double, y: Double): Vector2d {
		val relativeX = (-viewportWidth / 2.0 + x) / settings.zoom / PIXELS_IN_STARBOUND_UNIT + camera.pos.x
		val relativeY = (-viewportHeight / 2.0 + y) / settings.zoom / PIXELS_IN_STARBOUND_UNIT + camera.pos.y
		return Vector2d(relativeX, relativeY)
	}

	fun screenToWorld(x: Int, y: Int): Vector2d {
		return screenToWorld(x.toDouble(), y.toDouble())
	}

	fun screenToWorld(value: Vector2d): Vector2d {
		return screenToWorld(value.x, value.y)
	}

	val tileRenderers = TileRenderers(this)
	var world: ClientWorld? = null

	val settings = ClientSettings()

	val viewportCells: ICellAccess = object : ICellAccess {
		override fun getCell(x: Int, y: Int): AbstractCell {
			return world!!.getCell(x + viewportCellX, y + viewportCellY)
		}

		override fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean {
			return world!!.setCell(x + viewportCellX, y + viewportCellY, cell, chunkState)
		}

		override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
			return world!!.setCell(x + viewportCellX, y + viewportCellY, cell)
		}
	}

	var viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight)
		private set

	var viewportLightingTexture: GLTexture2D by Delegates.notNull()
		private set

	private var viewportLightingMem: ByteBuffer? = null

	fun updateViewportParams() {
		viewportRectangle = AABB.rectangle(
			camera.pos,
			viewportWidth / settings.zoom / PIXELS_IN_STARBOUND_UNIT,
			viewportHeight / settings.zoom / PIXELS_IN_STARBOUND_UNIT
		)

		viewportCellX = roundTowardsNegativeInfinity(viewportRectangle.mins.x) - 16
		viewportCellY = roundTowardsNegativeInfinity(viewportRectangle.mins.y) - 16
		val viewportCellWidth0 = roundTowardsPositiveInfinity(viewportRectangle.width) + 32
		val viewportCellHeight0 = roundTowardsPositiveInfinity(viewportRectangle.height) + 32

		if ((viewportCellWidth0 - viewportCellWidth).absoluteValue > 1)
			viewportCellWidth = viewportCellWidth0

		if ((viewportCellHeight0 - viewportCellHeight).absoluteValue > 1)
			viewportCellHeight = viewportCellHeight0

		viewportTopRight = screenToWorld(viewportWidth, viewportHeight)
		viewportBottomLeft = screenToWorld(0, 0)

		if (viewportLighting.width != viewportCellWidth || viewportLighting.height != viewportCellHeight) {
			viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight)
			viewportLighting.multithreaded = true

			if (viewportCellWidth > 0 && viewportCellHeight > 0) {
				viewportLightingMem = ByteBuffer.allocateDirect(viewportCellWidth.coerceAtMost(4096) * viewportCellHeight.coerceAtMost(4096) * 3)
				viewportLightingTexture = GLTexture2D(viewportCellWidth.coerceAtMost(4096), viewportCellHeight.coerceAtMost(4096), GL_RGB8)
			} else {
				viewportLightingMem = null
			}
		}
	}

	fun onDrawGUI(lambda: () -> Unit) {
		onDrawGUI.add(lambda)
	}

	private val layers = LayeredRenderer(this)
	private var dotsIndex = 0
	private val dotTime = 7
	private var dotCountdown = dotTime
	private var dotInc = 1

	private fun drawPerformanceBasic(onlyMemory: Boolean) {
		val runtime = Runtime.getRuntime()

		//if (!onlyMemory) font.render("Latency: ${(spinner.averageRenderWait * 1_00000.0).toInt() / 100f}ms", scale = 0.4f)
		//if (!onlyMemory) font.render("Frame: ${(spinner.averageRenderTime * 1_00000.0).toInt() / 100f}ms", y = font.lineHeight * 0.6f, scale = 0.4f)
		font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = font.lineHeight * 1.2f, scale = 0.4f)
		if (!onlyMemory) font.render("OGL C: $openglObjectsCreated D: $openglObjectsCleaned A: ${openglObjectsCreated - openglObjectsCleaned}", y = font.lineHeight * 1.8f, scale = 0.4f)
	}

	private var renderedLoadingScreen = false

	private fun renderLoadingScreen() {
		performOpenGLCleanup()
		updateViewportParams()

		clearColor = RGBAColor.BLACK
		glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)

		val min = viewportHeight.coerceAtMost(viewportWidth)
		val size = min * 0.02f

		val program = programs.positionColor
		val builder = program.builder

		uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }
		fontShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }

		stack.clear(Matrix3f.identity())

		program.colorMultiplier = RGBAColor.WHITE

		builder.builder.begin(GeometryType.QUADS)

		if (dotCountdown-- <= 0) {
			dotCountdown = dotTime
			dotsIndex += dotInc

			if (dotsIndex < 0) {
				dotsIndex = 1
				dotInc = 1
			} else if (dotsIndex >= 3) {
				dotsIndex = 1
				dotInc = -1
			}
		}

		val a = if (dotsIndex == 0) RGBAColor.SLATE_GRAY else RGBAColor.WHITE
		val b = if (dotsIndex == 1) RGBAColor.SLATE_GRAY else RGBAColor.WHITE
		val c = if (dotsIndex == 2) RGBAColor.SLATE_GRAY else RGBAColor.WHITE

		builder.builder.centeredQuad(viewportWidth * 0.5f, viewportHeight * 0.5f, size, size) { color(b) }
		builder.builder.centeredQuad(viewportWidth * 0.5f - size * 2f, viewportHeight * 0.5f, size, size) { color(a) }
		builder.builder.centeredQuad(viewportWidth * 0.5f + size * 2f, viewportHeight * 0.5f, size, size) { color(c) }

		builder.builder.quad(0f, viewportHeight - 20f, viewportWidth * Starbound.loadingProgress.toFloat(), viewportHeight.toFloat()) { color(RGBAColor.GREEN) }

		GLFW.glfwSetWindowTitle(window, "KStarbound: ${Starbound.loadingProgressText}")
		renderedLoadingScreen = true

		val runtime = Runtime.getRuntime()

		//if (runtime.maxMemory() <= 4L * 1024L * 1024L * 1024L) {
		builder.builder.centeredQuad(viewportWidth * 0.5f, viewportHeight * 0.1f, viewportWidth * 0.7f, 2f) { color(RGBAColor.WHITE) }
		builder.builder.centeredQuad(viewportWidth * 0.5f, viewportHeight * 0.1f + 20f, viewportWidth * 0.7f, 2f) { color(RGBAColor.WHITE) }
		builder.builder.centeredQuad(viewportWidth * (0.5f - 0.35f), viewportHeight * 0.1f + 10f, 2f, 20f) { color(RGBAColor.WHITE) }
		builder.builder.centeredQuad(viewportWidth * (0.5f + 0.35f), viewportHeight * 0.1f + 10f, 2f, 20f) { color(RGBAColor.WHITE) }

		builder.builder.centeredQuad(viewportWidth * (0.5f - 0.35f + 0.7f * (runtime.totalMemory().toDouble() / runtime.maxMemory().toDouble()).toFloat()), viewportHeight * 0.1f + 10f, 2f, 18f) { color(RGBAColor.RED) }

		val leftEdge = viewportWidth * (0.5f - 0.35f) + 1f

		builder.builder.quad(
			leftEdge,
			viewportHeight * 0.1f + 1f,
			leftEdge + viewportWidth * 0.7f * ((runtime.totalMemory() - runtime.freeMemory()) / runtime.maxMemory().toDouble()).toFloat(),
			viewportHeight * 0.1f + 19f
		) { color(RGBAColor(29, 140, 160)) }
		//}

		if (fontInitialized) {
			drawPerformanceBasic(true)
		}

		program.use()
		builder.upload()
		builder.draw()

		GLFW.glfwSwapBuffers(window)
		GLFW.glfwPollEvents()
	}

	private fun renderWorld(world: ClientWorld) {
		updateViewportParams()
		world.tick(Starbound.TIMESTEP)

		stack.clear(Matrix3f.identity())

		val viewMatrix = viewportMatrixWorld.copy()
			.translate(viewportWidth / 2f, viewportHeight / 2f) // центр экрана + координаты отрисовки мира
			.scale(x = settings.zoom * PIXELS_IN_STARBOUND_UNITf, y = settings.zoom * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера
			.translate(-camera.pos.x.toFloat(), -camera.pos.y.toFloat()) // перемещаем вид к камере

		uberShaderPrograms.forValidRefs { it.viewMatrix = viewMatrix }
		fontShaderPrograms.forValidRefs { it.viewMatrix = viewMatrix }

		viewportLighting.clear()
		val viewportLightingMem = viewportLightingMem

		world.lock.withLock {
			world.addLayers(
				layers = layers,
				size = viewportRectangle)
		}

		if (viewportLightingMem != null && !fullbright) {
			val spos = screenToWorld(mouseCoordinates)

			viewportLighting.addPointLight(roundTowardsPositiveInfinity(spos.x - viewportCellX), roundTowardsPositiveInfinity(spos.y - viewportCellY), 1f, 1f, 1f)

			viewportLightingMem.position(0)
			BufferUtils.zeroBuffer(viewportLightingMem)
			viewportLightingMem.position(0)
			viewportLighting.calculate(viewportLightingMem, viewportLightingTexture.width, viewportLightingTexture.height)
			viewportLightingMem.position(0)

			val old = textureUnpackAlignment
			textureUnpackAlignment = if (viewportLightingTexture.width.coerceAtMost(4096) % 4 == 0) 4 else 1

			viewportLightingTexture.upload(
				GL_RGB,
				GL_UNSIGNED_BYTE,
				viewportLightingMem
			)

			textureUnpackAlignment = old
		} else {
			viewportLightingTexture = whiteTexture
		}

		viewportLightingTexture.textureMinFilter = GL_LINEAR
		textures2D[lightMapLocation] = viewportLightingTexture

		val lightmapUV = if (fullbright) Vector4f.ZERO else Vector4f(
			((viewportBottomLeft.x - viewportCellX) / viewportLighting.width).toFloat(),
			((viewportBottomLeft.y - viewportCellY) / viewportLighting.height).toFloat(),
			(1f - (viewportCellX + viewportCellWidth - viewportTopRight.x) / viewportLighting.width).toFloat(),
			(1f - (viewportCellY + viewportCellHeight - viewportTopRight.y) / viewportLighting.height).toFloat())

		uberShaderPrograms.forValidRefs {
			it.lightmapTexture = lightMapLocation
			it.lightmapUV = lightmapUV
		}
	}

	private fun renderFrame() {
		val world = world

		if (!isRenderingGame) {
			performOpenGLCleanup()
			GLFW.glfwPollEvents()

			if (world != null && Starbound.initialized)
				world.tick(Starbound.TIMESTEP)

			activeConnection?.tick()
			return
		}

		if (!Starbound.initialized || !fontInitialized) {
			performOpenGLCleanup()
			renderLoadingScreen()
			return
		}

		if (renderedLoadingScreen) {
			renderedLoadingScreen = false
			GLFW.glfwSetWindowTitle(window, "KStarbound")
		}

		input.think()
		camera.think(Starbound.TIMESTEP)
		performOpenGLCleanup()

		layers.clear()

		uberShaderPrograms.forValidRefs {
			if (it.flags.contains(UberShader.Flag.NEEDS_SCREEN_SIZE)) {
				it.screenSize = Vector2f(viewportWidth.toFloat(), viewportHeight.toFloat())
			}
		}

		clearColor = RGBAColor.SLATE_GRAY
		glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)

		if (world != null) {
			renderWorld(world)
		}

		layers.render()

		val activeConnection = activeConnection

		uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }
		fontShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }

		stack.clear(Matrix3f.identity())

		for (fn in onDrawGUI) {
			fn.invoke()
		}

		if (world != null) {
			font.render("Camera: ${camera.pos} ${settings.zoom}", y = 140f, scale = 0.25f)
			font.render("Cursor: $mouseCoordinates -> ${screenToWorld(mouseCoordinates)}", y = 160f, scale = 0.25f)
			font.render("World chunk: ${world.geometry.chunkFromCell(camera.pos)}", y = 180f, scale = 0.25f)
		}

		drawPerformanceBasic(false)

		GLFW.glfwSwapBuffers(window)
		GLFW.glfwPollEvents()

		performOpenGLCleanup()
		activeConnection?.tick()
	}

	private fun tick() {
		try {
			val ply = activeConnection?.character

			if (ply != null) {
				camera.pos = ply.position

				ply.movement.controlMove = if (input.KEY_A_DOWN) Direction.LEFT else if (input.KEY_D_DOWN) Direction.RIGHT else null
				ply.movement.controlJump = input.KEY_SPACE_DOWN
				ply.movement.controlRun = !input.KEY_LEFT_SHIFT_DOWN
			} else {
				camera.pos += Vector2d(
					(if (input.KEY_A_DOWN) -Starbound.TIMESTEP * 32f / settings.zoom else 0.0) + (if (input.KEY_D_DOWN) Starbound.TIMESTEP * 32f / settings.zoom else 0.0),
					(if (input.KEY_W_DOWN) Starbound.TIMESTEP * 32f / settings.zoom else 0.0) + (if (input.KEY_S_DOWN) -Starbound.TIMESTEP * 32f / settings.zoom else 0.0)
				)

				camera.pos = world?.geometry?.wrap(camera.pos) ?: camera.pos
			}

			renderFrame()
			performOpenGLCleanup()

			if (input.KEY_ESCAPE_PRESSED) {
				GLFW.glfwSetWindowShouldClose(window, true)
			}

			if (GLFW.glfwWindowShouldClose(window)) {
				close()
			}
		} catch (err: Throwable) {
			LOGGER.fatal("Exception in main game logic", err)
			shutdownNow()
		}
	}

	override fun performShutdown() {
		executor.shutdown()

		lock.lock()

		try {
			if (window != MemoryUtil.NULL) {
				Callbacks.glfwFreeCallbacks(window)
				GLFW.glfwDestroyWindow(window)
			}

			if (--clients == 0) {
				GLFW.glfwTerminate()
				GLFW.glfwSetErrorCallback(null)?.free()
				glfwInitialized = false
			}
		} catch (err: Throwable) {
			LOGGER.fatal("Exception while destroying client", err)
		} finally {
			lock.unlock()
		}
	}

	override fun close() {
		shutdown()
	}

	private fun initialize() {
		check(CLIENTS.get() == null) { "Already has OpenGL context existing at ${currentThread()}!" }
		CLIENTS.set(this)

		lock.lock()

		try {
			clients++

			if (!glfwInitialized) {
				check(GLFW.glfwInit()) { "Unable to initialize GLFW" }
				glfwInitialized = true

				GLFWErrorCallback.create { error, description ->
					LOGGER.error("LWJGL error {}: {}", error, description)
				}.set()
			}
		} finally {
			lock.unlock()
		}

		GLFW.glfwDefaultWindowHints()

		GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE)
		GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE)
		GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 4)
		GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 5)
		GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE)
		GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE)

		window = GLFW.glfwCreateWindow(800, 600, "KStarbound: Locating files...", MemoryUtil.NULL, MemoryUtil.NULL)
		require(window != MemoryUtil.NULL) { "Unable to create GLFW window" }

		input.installCallback(window)

		GLFW.glfwMakeContextCurrent(window)

		// This line is critical for LWJGL's interoperation with GLFW's
		// OpenGL context, or any context that is managed externally.
		// LWJGL detects the context that is current in the current thread,
		// creates the GLCapabilities instance and makes the OpenGL
		// bindings available for use.
		capabilities = GL.createCapabilities()

		GLFW.glfwSetFramebufferSizeCallback(window) { _, w, h ->
			if (w == 0 || h == 0) {
				isRenderingGame = false
			} else {
				isRenderingGame = true
				setViewport(0, 0, w, h)
				viewportMatrixScreen = updateViewportMatrixScreen()
				viewportMatrixWorld = updateViewportMatrixWorld()

				for (callback in onViewportChanged) {
					callback.invoke(w, h)
				}
			}
		}

		var stack = MemoryStack.stackPush()

		try {
			val pWidth = stack.mallocInt(1)
			val pHeight = stack.mallocInt(1)

			GLFW.glfwGetWindowSize(window, pWidth, pHeight)

			val vidmode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor())!!

			GLFW.glfwSetWindowPos(
				window,
				(vidmode.width() - pWidth[0]) / 2,
				(vidmode.height() - pHeight[0]) / 2
			)

			setViewport(0, 0, pWidth[0], pHeight[0])
			viewportMatrixScreen = updateViewportMatrixScreen()
			viewportMatrixWorld = updateViewportMatrixWorld()
		} finally {
			stack.close()
		}

		stack = MemoryStack.stackPush()

		try {
			val pWidth = stack.mallocInt(1)
			val pHeight = stack.mallocInt(1)
			val pChannels = stack.mallocInt(1)

			val readFromDisk = readInternalBytes("starbound_icon.png")
			val buff = ByteBuffer.allocateDirect(readFromDisk.size)
			buff.put(readFromDisk)
			buff.position(0)

			val data = STBImage.stbi_load_from_memory(buff, pWidth, pHeight, pChannels, 4) ?: throw IllegalStateException("Unable to decode starbound_icon.png")
			val img = GLFWImage.malloc()
			img.set(pWidth[0], pHeight[0], data)

			GLFW.nglfwSetWindowIcon(window, 1, memAddressSafe(img))
			img.free()
		} finally {
			stack.close()
		}

		// vsync
		GLFW.glfwSwapInterval(0)

		GLFW.glfwShowWindow(window)

		maxTextureBlocks = glGetInteger(GL_MAX_TEXTURE_IMAGE_UNITS)
		maxUserTextureBlocks = maxTextureBlocks - 1 // available textures blocks for generic use
		maxVertexAttribBindPoints = glGetInteger(GL_MAX_VERTEX_ATTRIB_BINDINGS)

		textures2D = GLTexturesTracker(maxTextureBlocks)

		LOGGER.info("OpenGL Version: ${glGetString(GL_VERSION)}")
		LOGGER.info("OpenGL Vendor: ${glGetString(GL_VENDOR)}")
		LOGGER.info("OpenGL Renderer: ${glGetString(GL_RENDERER)}")
		LOGGER.debug("Max supported texture image units: $maxTextureBlocks")
		LOGGER.debug("Max supported vertex attribute bind points: $maxVertexAttribBindPoints")

		lightMapLocation = maxTextureBlocks - 1

		clearColor = RGBAColor.SLATE_GRAY

		blend = true
		blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA

		viewportLightingTexture = GLTexture2D(1, 1, GL_RGB8)

		Starbound.loadFont().thenAcceptAsync(Consumer {
			try {
				font = Font(it.canonicalPath)
				fontInitialized = true
			} catch (err: Throwable) {
				LOGGER.fatal("Unable to load font", err)
			}
		}, this)
	}

	init {
		CLIENTS.remove()

		input.addScrollCallback { _, x, y ->
			if (y > 0.0) {
				settings.zoom *= y.toFloat() * 2f
			} else if (y < 0.0) {
				settings.zoom /= -y.toFloat() * 2f
			}
		}

		execute { initialize() }
		scheduleAtFixedRate(::tick, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
		start()
	}

	override fun toString(): String {
		return "StarboundClient[$clientID]"
	}

	companion object {
		private val COUNTER = AtomicInteger()
		private val LOGGER = LogManager.getLogger(StarboundClient::class.java)
		private val CLIENTS = ThreadLocal<StarboundClient>()
		private val WHITE = ByteBuffer.allocateDirect(4).also {
			it.put(0xFF.toByte())
			it.put(0xFF.toByte())
			it.put(0xFF.toByte())
			it.put(0xFF.toByte())
			it.position(0)
		}

		@JvmStatic
		fun current() = checkNotNull(CLIENTS.get()) { "No client registered to current thread (${Thread.currentThread()})" }
		@JvmStatic
		fun currentOrNull(): StarboundClient? = CLIENTS.get()

		private val lock = ReentrantLock()
		@Volatile
		private var glfwInitialized = false
		@Volatile
		private var clients = 0

		@JvmStatic
		fun readInternal(file: String): String {
			return ClassLoader.getSystemClassLoader().getResourceAsStream(file)!!.bufferedReader()
				.let {
					val read = it.readText()
					it.close()
					read
				}
		}

		@JvmStatic
		fun readInternalBytes(file: String): ByteArray {
			return ClassLoader.getSystemClassLoader().getResourceAsStream(file)!!.readAllBytes()
		}
	}
}
