package ru.dbotthepony.mc.otm.client.screen.panels.input

import com.mojang.blaze3d.platform.InputConstants
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.DefaultVertexFormat
import com.mojang.blaze3d.vertex.VertexFormat
import it.unimi.dsi.fastutil.chars.Char2IntFunction
import it.unimi.dsi.fastutil.chars.Char2IntOpenHashMap
import it.unimi.dsi.fastutil.chars.CharOpenHashSet
import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.screens.Screen
import net.minecraft.client.renderer.GameRenderer
import net.minecraft.network.chat.Component
import org.joml.Vector2i
import ru.dbotthepony.mc.otm.client.isCtrlDown
import ru.dbotthepony.mc.otm.client.isShiftDown
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.client.playGuiClickSound
import ru.dbotthepony.mc.otm.client.render.DynamicBufferSource
import ru.dbotthepony.mc.otm.client.render.RenderGravity
import ru.dbotthepony.mc.otm.client.render.drawAligned
import ru.dbotthepony.mc.otm.client.render.renderRect
import ru.dbotthepony.mc.otm.client.render.tesselator
import ru.dbotthepony.mc.otm.client.screen.panels.DockProperty
import ru.dbotthepony.mc.otm.client.screen.panels.EditablePanel
import ru.dbotthepony.mc.otm.core.TextComponent
import ru.dbotthepony.mc.otm.core.addAll
import ru.dbotthepony.mc.otm.core.math.RGBAColor
import ru.dbotthepony.mc.otm.milliTime
import kotlin.math.roundToInt

open class TextInputPanel<out S : Screen>(
	screen: S,
	parent: EditablePanel<*>?,
	x: Float = 0f,
	y: Float = 0f,
	width: Float = 60f,
	height: Float = 11f,
) : EditablePanel<S>(screen, parent, x, y, width, height) {
	private data class TextSelection(val cursor: Int, val shift: Int) {
		val isLeft get() = shift < 0
		val isRight get() = shift > 0

		fun move(value: Int, lineLength: Int): TextSelection {
			return TextSelection(cursor, (shift + value).coerceIn(-cursor, lineLength - cursor + 1))
		}

		val isValid get() = shift != 0
		val start get() = if (isRight) cursor else cursor + shift
		val end get() = if (isRight) cursor + shift else cursor

		fun sub(line: String): Pair<String, String> {
			val before = line.substring(0, start.coerceIn(0, line.length))
			val selected = line.substring(start.coerceIn(0, line.length), end.coerceAtMost(line.length))

			return before to selected
		}

		fun coversEntireString(value: String): Boolean {
			return start <= 0 && value.length <= end
		}

		fun coversEntireLine(value: String): Boolean {
			return start <= 0 && value.length < end
		}

		fun coversNewline(value: String): Boolean {
			return value.length < end
		}
	}

	private fun putSelection(index: Int, selection: TextSelection) {
		if (selection.isValid) {
			selections[index] = selection
		} else {
			selections.remove(index)
		}
	}

	private inner class Snapshot {
		private val lines = ArrayList(this@TextInputPanel.lines) // ultra fast copy
		private val cursorLine = this@TextInputPanel.cursorLine
		private val cursorCharacter = this@TextInputPanel.cursorRow
		private val selections = Int2ObjectAVLTreeMap(this@TextInputPanel.selections)
		private val multiLine = this@TextInputPanel.multiLine

		fun apply() {
			this@TextInputPanel.lines.clear()

			if (this@TextInputPanel.multiLine)
				this@TextInputPanel.lines.addAll(lines)
			else
				this@TextInputPanel.lines.add(lines.joinToString(""))


			this@TextInputPanel.selections.clear()
			if (this@TextInputPanel.multiLine && multiLine)
				this@TextInputPanel.selections.putAll(selections)

			this@TextInputPanel.cursorRow = cursorCharacter
			this@TextInputPanel.cursorLine = cursorLine
			triggerChangeCallback()
		}

		override fun equals(other: Any?): Boolean {
			if (this === other) return true
			if (javaClass != other?.javaClass) return false

			other as TextInputPanel<*>.Snapshot

			if (lines != other.lines) return false
			if (cursorLine != other.cursorLine) return false
			if (cursorCharacter != other.cursorCharacter) return false
			if (selections != other.selections) return false

			return true
		}

		override fun hashCode(): Int {
			var result = lines.hashCode()
			result = 31 * result + cursorLine
			result = 31 * result + cursorCharacter
			return result
		}
	}

	var debugDraw = false
	var multiLine = false
		set(value) {
			if (field == value) return

			if (!value && lines.size > 1) {
				val merge = lines.joinToString("")
				lines.clear()
				lines.add(merge)
				triggerChangeCallback()
			}

			field = value
		}

	var cursorLine = 0
	var cursorRow = 0
	open var placeholder: Component = TextComponent("")
	open var textColor = RGBAColor.WHITE
	open var placeholderColor = RGBAColor.DARK_GRAY
	open var cursorColor = RGBAColor.GREEN
	open var backgroundColor = RGBAColor.BLACK
	open var isActive = true

	init {
		scissor = true
		dockPadding = DockProperty(2f, 2f, 2f, 2f)
	}

	private var oldText = ArrayList<String>()
	private var textCache: String? = null
	private val lines = ArrayList<String>()
	private val selections = Int2ObjectAVLTreeMap<TextSelection>()
	private val undo = ArrayDeque<Snapshot>()
	private val redo = ArrayDeque<Snapshot>()

	/**
	 * scroll in rows (up-down)
	 */
	private var scrollLines = 0

	/**
	 * scroll in pixels (left-right)
	 */
	private var scrollPixels = 0f

	var rowSpacing = 2f

	private fun triggerChangeCallback() {
		if (oldText != lines) {
			textCache = null
			val old = oldText.joinToString("\n")
			oldText = ArrayList(lines)
			onTextChanged(old, textCache())
		}
	}

	private var snapshotTimer: Long? = null

	override fun tickInner() {
		super.tickInner()

		if (snapshotTimer != null && snapshotTimer!! <= milliTime) {
			pushbackSnapshot()
			snapshotTimer = null
			redo.clear()
		}
	}

	private fun pushbackSnapshot() {
		val snapshot = Snapshot()

		if (undo.isEmpty() || undo.last() != snapshot)
			undo.addLast(snapshot)
	}

	private fun pushbackSnapshotIfNoTimer() {
		if (snapshotTimer == null)
			pushbackSnapshot()
	}

	private fun recordHistory(hard: Boolean = false) {
		if (hard) {
			pushbackSnapshot()
			snapshotTimer = null
		} else if (snapshotTimer == null) {
			snapshotTimer = milliTime + 800L
		} else if (snapshotTimer!! <= milliTime) {
			pushbackSnapshot()
			snapshotTimer = null
		}

		redo.clear()
	}

	// it gets really dirty
	// idk man :(
	private fun undo() {
		if (undo.isEmpty())
			return

		if (snapshotTimer != null || redo.isEmpty()) {
			pushbackSnapshot()
			snapshotTimer = null
		}

		val current = Snapshot()

		while (undo.isNotEmpty()) {
			val removed = undo.removeLast()
			redo.addFirst(removed)

			if (removed != current) {
				removed.apply()
				break
			}
		}
	}

	// it gets really dirty
	// idk man :(
	private fun redo() {
		if (redo.isEmpty())
			return

		val current = Snapshot()

		snapshotTimer = null

		while (redo.isNotEmpty()) {
			val removed = redo.removeFirst()
			undo.addLast(removed)

			if (current != removed) {
				removed.apply()
				break
			}
		}
	}

	operator fun get(index: Int): String? {
		return lines.getOrNull(index)
	}

	operator fun set(index: Int, value: String) {
		if (index < 0)
			throw IndexOutOfBoundsException("negative index $index")

		if (!multiLine && index != 0)
			throw IllegalStateException("Not accepting newlines")

		lines.ensureCapacity(index)

		while (lines.size <= index) {
			lines.add("")
		}

		lines[index] = value
	}

	private fun selectionIndex(index: Int) = object : Int2ObjectMap.Entry<TextSelection> {
		override fun setValue(newValue: TextSelection): TextSelection? {
			return selections.put(index, newValue)
		}

		override fun getIntKey(): Int {
			return index
		}

		override val value: TextSelection
			get() = selections[index] ?: throw NoSuchElementException()
	}

	fun insertLine(index: Int, value: String = "") {
		if (!multiLine && lines.isNotEmpty())
			throw IllegalStateException("Not accepting newlines")

		lines.ensureCapacity(index)

		while (lines.size < index) {
			lines.add("")
		}

		val upperLines = ArrayList<Int2ObjectMap.Entry<TextSelection>>()
		upperLines.addAll(selections.int2ObjectEntrySet().iterator(selectionIndex(index)))

		for (entry in upperLines) {
			selections.remove(entry.intKey)
		}

		for (entry in upperLines) {
			selections.put(entry.intKey + 1, entry.value)
		}

		lines.add(index, value)
	}

	fun removeLine(index: Int): Boolean {
		if (index < 0 || index >= lines.size)
			return false

		if (cursorLine == index) {
			if (index != 0)
				cursorLine--

			cursorRow = lines.getOrNull(cursorLine)?.length ?: 0
		}

		lines.removeAt(index)
		selections.remove(index)

		val upperLines = ArrayList<Int2ObjectMap.Entry<TextSelection>>()
		upperLines.addAll(selections.int2ObjectEntrySet().iterator(selectionIndex(index)))

		for (entry in upperLines) {
			selections.remove(entry.intKey)
		}

		for (entry in upperLines) {
			selections.put(entry.intKey - 1, entry.value)
		}

		return true
	}

	protected fun moveCursors(line: Int, character: Int, moveBy: Int) {
		@Suppress("name_shadowing")
		val moveBy = if (character + moveBy < 0) -character else moveBy

		if (cursorLine == line && cursorRow >= character) {
			cursorRow = (cursorRow + moveBy).coerceIn(0, this[cursorLine]?.length ?: Int.MAX_VALUE)
		}

		val selection = selections[line]

		if (selection != null) {
			val start = selection.start
			val end = selection.end

			if (character < start) {
				putSelection(line, TextSelection(start + moveBy, end + moveBy))
			} else if (character > end && character + moveBy < end) {
				if (character + moveBy < start) {
					// selections.remove(line)
					selections[line] = TextSelection(0, 0)
				} else {
					putSelection(line, TextSelection(start, character + moveBy))
				}
			} else if (character in start .. end) {
				if (moveBy > 0 || character + moveBy <= start) {
					putSelection(line, TextSelection(start, end + moveBy))
				} else {
					putSelection(line, TextSelection(character + moveBy, end + moveBy))
				}
			}
		}
	}

	protected fun wipeSelection() {
		if (selections.isEmpty())
			return

		pushbackSnapshotIfNoTimer()
		val set = selections.int2ObjectEntrySet()

		while (set.isNotEmpty()) {
			val (lineNumber, selection) = set.last()
			selections.remove(lineNumber)
			val line = this[lineNumber] ?: continue
			if (!selection.isValid) continue // ???

			if (selection.coversEntireLine(line)) {
				removeLine(lineNumber)
			} else if (selection.coversEntireString(line)) {
				this[lineNumber] = ""
			} else if (selection.coversNewline(line)) {
				val before = line.substring(0, selection.start.coerceIn(0, line.length))
				val next = this[lineNumber + 1]

				if (next == null) {
					this[lineNumber] = before
				} else {
					this[lineNumber] = before + next
					removeLine(lineNumber + 1)
				}
			} else {
				val before = line.substring(0, selection.start.coerceIn(0, line.length))
				val after = line.substring(selection.end.coerceIn(0, line.length))
				this[lineNumber] = before + after
			}

			if (lineNumber < cursorLine) {
				cursorLine = lineNumber
				cursorRow = selection.start
			} else if (lineNumber == cursorLine && cursorRow > selection.start) {
				cursorRow = selection.start
			}
		}

		triggerChangeCallback()
	}

	private fun textCache(): String {
		var textCache = textCache

		if (textCache == null) {
			textCache = lines.joinToString("\n")
			this.textCache = textCache
		}

		return textCache
	}

	var text: String
		get() {
			return textCache()
		}
		set(value) {
			if (textCache() == value)
				return

			snapshotTimer = null
			selections.clear()
			lines.clear()
			redo.clear()
			undo.clear()
			cursorLine = 0
			cursorRow = 0
			textCache = null

			if (multiLine) {
				lines.addAll(value.split(NEWLINES))
			} else {
				lines.add(value.replace(NEWLINES, ""))
			}
		}

	data class CursorAdvanceResult(
		val oldLine: Int,
		val newLine: Int,
		val oldCharacter: Int,
		val newCharacter: Int,
		val couldHaveChangedLine: Boolean
	) {
		val linesChanged get() = oldLine != newLine
		val charsChanged get() = linesChanged || oldCharacter != newCharacter
		val advancedChars get() = newCharacter - oldCharacter
	}

	fun advanceCursorLeft(greedy: Boolean = false): CursorAdvanceResult {
		val oldLine = cursorLine
		val line = this[cursorLine]
		var couldHaveChangedLine = false

		if (line != null && cursorRow > line.length) {
			cursorRow = line.length
		} else if (cursorRow < 0) {
			cursorRow = 0
		}

		val oldChar = cursorRow

		if (cursorRow > 0) {
			if (greedy && line != null) {
				cursorRow = greedyAdvanceLeft(line, cursorRow)
			} else {
				cursorRow--
			}
		} else if (cursorLine > 0) {
			cursorLine--
			cursorRow = 0
			couldHaveChangedLine = true

			@Suppress("name_shadowing")
			val line = this[cursorLine]

			if (line != null) {
				cursorRow = line.length
			}
		} else {
			couldHaveChangedLine = true
		}

		return CursorAdvanceResult(oldLine, cursorLine, oldChar, cursorRow, couldHaveChangedLine = couldHaveChangedLine)
	}

	fun advanceCursorRight(greedy: Boolean = false): CursorAdvanceResult {
		val oldLine = cursorLine
		var couldHaveChangedLine = false

		val line = this[cursorLine]

		if (line != null && cursorRow > line.length) {
			cursorRow = line.length
		} else if (cursorRow < 0) {
			cursorRow = 0
		}

		val oldChar = cursorRow

		if (greedy && line != null && cursorRow + 1 < line.length) {
			cursorRow = greedyAdvanceRight(line, cursorRow)
		} else {
			cursorRow++
		}

		if (line != null && cursorRow > line.length) {
			couldHaveChangedLine = true

			if (lines.size <= cursorLine + 1) {
				cursorRow = line.length
			} else {
				cursorLine++
				cursorRow = 0
			}
		} else if (line == null) {
			cursorRow = 0
		}

		return CursorAdvanceResult(oldLine, cursorLine, oldChar, cursorRow, couldHaveChangedLine = couldHaveChangedLine)
	}

	private fun simulateSelectLeft(greedy: Boolean) {
		val line = this[cursorLine] ?: return
		val existing = selections[cursorLine] ?: TextSelection(cursorRow, 0)
		val result = advanceCursorLeft(greedy)

		if (result.couldHaveChangedLine) {
			putSelection(result.oldLine, existing.move(-line.length, line.length))
		} else {
			putSelection(result.oldLine, existing.move(result.newCharacter - result.oldCharacter, line.length))
		}

		if (result.linesChanged) {
			val existingNew = selections[result.newLine]
			val lineNew = this[result.newLine] ?: return

			if (existingNew == null) {
				putSelection(result.newLine, TextSelection(result.newCharacter + 1, -1))
			} else {
				putSelection(result.newLine, existingNew.move(-1, lineNew.length))
			}
		}
	}

	private fun simulateSelectRight(greedy: Boolean) {
		val line = this[cursorLine] ?: return
		val existing = selections[cursorLine] ?: TextSelection(cursorRow, 0)
		val result = advanceCursorRight(greedy)

		if (result.couldHaveChangedLine) {
			if (result.oldLine != lines.size - 1)
				putSelection(result.oldLine, existing.move(line.length.coerceAtLeast(1), line.length))
		} else {
			putSelection(result.oldLine, existing.move(result.newCharacter - result.oldCharacter, line.length))
		}
	}

	private fun simulateSelectionUp(): Boolean {
		if (cursorLine <= 0)
			return false

		val cursorCharacter = cursorRow
		val cursorLine = cursorLine

		while (this.cursorRow >= 0 && this.cursorLine == cursorLine) {
			simulateSelectLeft(false)
		}

		val line = this[this.cursorLine]!!

		if (cursorCharacter < line.length) {
			this.cursorRow = line.length

			while (this.cursorRow > cursorCharacter) {
				simulateSelectLeft(false)
			}
		}

		this.cursorRow = cursorCharacter
		return true
	}

	private fun simulateSelectionDown(): Boolean {
		if (cursorLine >= lines.size - 1)
			return false

		val cursorCharacter = cursorRow
		val cursorLine = cursorLine

		while (this.cursorRow >= cursorCharacter && this.cursorLine == cursorLine) {
			simulateSelectRight(true)
		}

		val line = this[this.cursorLine]!!

		if (cursorCharacter < line.length) {
			this.cursorRow = 0

			while (this.cursorRow < cursorCharacter) {
				simulateSelectRight(false)
			}
		} else {
			if (line.isEmpty()) {
				val existing = selections[this.cursorLine]

				if (existing != null && existing.isLeft) {
					selections.remove(this.cursorLine)
				}
			} else {
				val store = this.cursorLine

				for (i in 0 .. line.length) {
					simulateSelectRight(false)
				}

				this.cursorLine = store
			}
		}

		this.cursorRow = cursorCharacter
		return true
	}

	override fun keyPressedInternal(key: Int, scancode: Int, mods: Int): Boolean {
		if (key == InputConstants.KEY_ESCAPE || !isActive) {
			killFocus()
			return true
		}

		if (key == InputConstants.KEY_RETURN) {
			if (multiLine) {
				if (!minecraft.window.isShiftDown && !minecraft.window.isCtrlDown)
					wipeSelection()

				val line = this[cursorLine]

				if (line != null && !minecraft.window.isShiftDown) {
					if (cursorRow <= 0) {
						recordHistory(true)
						insertLine(cursorLine, "")

						if (!minecraft.window.isCtrlDown) {
							cursorLine++
							cursorRow = 0
						}
					} else if (cursorRow >= line.length) {
						recordHistory(true)
						insertLine(cursorLine + 1, "")

						if (!minecraft.window.isCtrlDown) {
							cursorLine++
							cursorRow = 0
						}
					} else {
						val before = line.substring(0, cursorRow)
						val after = line.substring(cursorRow)

						recordHistory(true)
						this[cursorLine] = before
						insertLine(cursorLine + 1, after)

						if (!minecraft.window.isCtrlDown) {
							cursorLine++
							cursorRow = 0
						}
					}
				} else {
					recordHistory(true)
					insertLine(cursorLine + 1)
					cursorLine++
					cursorRow = 0
				}

				triggerChangeCallback()

				return true
			} else {
				killFocus()
				triggerChangeCallback()
				return true
			}
		}

		if (key == InputConstants.KEY_LEFT) {
			if (!minecraft.window.isShiftDown) {
				advanceCursorLeft(minecraft.window.isCtrlDown)
				selections.clear()
			} else {
				if (this[cursorLine] == null)
					return true

				simulateSelectLeft(minecraft.window.isCtrlDown)
			}

			return true
		} else if (key == InputConstants.KEY_RIGHT) {
			if (!minecraft.window.isShiftDown) {
				advanceCursorRight(minecraft.window.isCtrlDown)
				selections.clear()
			} else {
				if (this[cursorLine] == null)
					return true

				simulateSelectRight(minecraft.window.isCtrlDown)
			}

			return true
		} else if (key == InputConstants.KEY_UP) {
			if (minecraft.window.isShiftDown) {
				simulateSelectionUp()
			} else {
				if (cursorLine > 0) {
					cursorLine--
				}

				selections.clear()
			}

			return true
		} else if (key == InputConstants.KEY_DOWN) {
			if (minecraft.window.isShiftDown) {
				simulateSelectionDown()
			} else {
				if (cursorLine < lines.size - 1) {
					cursorLine++
				}

				selections.clear()
			}

			return true
		}

		if (key == InputConstants.KEY_BACKSPACE) {
			if (selections.isNotEmpty()) {
				wipeSelection()
				return true
			}

			val line = this[cursorLine]

			if (cursorLine <= 0 && cursorRow <= 0) {
				return true
			} else if (cursorRow <= 0 || line?.length == 0) {
				if (line == null) {
					cursorRow = this[--cursorLine]?.length ?: 0
					recordHistory()
				} else {
					removeLine(cursorLine)
					val newLine = this[cursorLine]

					if (newLine == null) {
						this[cursorLine] = line
					} else {
						this[cursorLine] = newLine + line
					}

					recordHistory()
					triggerChangeCallback()
				}
			} else {
				if (line != null) {
					pushbackSnapshotIfNoTimer()

					// remove from very end
					if (cursorRow >= line.length) {
						val newLine = line.substring(0, line.length - 1)
						moveCursors(cursorLine, cursorRow, -1)
						this[cursorLine] = newLine
					} else {
						val newLine = line.substring(0, cursorRow - 1) + line.substring(cursorRow)
						moveCursors(cursorLine, cursorRow, -1)
						this[cursorLine] = newLine
					}

					recordHistory()
					triggerChangeCallback()
				} else if (cursorLine > 0) {
					cursorLine--
					recordHistory()
				}
			}

			return true
		}

		if (key == InputConstants.KEY_DELETE) {
			if (selections.isNotEmpty()) {
				wipeSelection()
				return true
			}

			if (cursorLine !in 0 until lines.size) {
				cursorLine = (lines.size - 1).coerceAtLeast(0)
				return true
			}

			if (minecraft.window.isShiftDown) {
				pushbackSnapshot()
				removeLine(cursorLine)

				if (cursorLine >= lines.size)
					cursorLine = lines.size

				cursorRow = 0
				recordHistory(true)
				triggerChangeCallback()
				return true
			}

			val line = this[cursorLine]!!

			if (cursorRow >= line.length) {
				if (cursorLine + 1 == lines.size) {
					return true
				}

				pushbackSnapshotIfNoTimer()

				val bottomLine = this[cursorLine + 1]!!
				cursorRow = line.length
				this[cursorLine] = line + bottomLine
				removeLine(cursorLine + 1)
			} else {
				pushbackSnapshotIfNoTimer()

				val cursorCharacter = cursorRow
				moveCursors(cursorLine, cursorCharacter, -1)
				this[cursorLine] = line.substring(0, cursorCharacter) + line.substring(cursorCharacter + 1)

				if (cursorCharacter != 0)
					this.cursorRow++
			}

			recordHistory()
			triggerChangeCallback()
			return true
		}

		if (key == InputConstants.KEY_Z && minecraft.window.isCtrlDown) {
			undo()
			return true
		} else if (key == InputConstants.KEY_Y && minecraft.window.isCtrlDown) {
			redo()
			return true
		}

		if (key == InputConstants.KEY_A && minecraft.window.isCtrlDown) {
			selections.clear()

			for ((i, line) in lines.withIndex()) {
				selections[i] = TextSelection(0, line.length + 1)
			}

			if (lines.isNotEmpty()) {
				cursorLine = lines.size - 1
				cursorRow = lines[lines.size - 1].length
			}

			return true
		}

		if (key == InputConstants.KEY_V && minecraft.window.isCtrlDown) {
			wipeSelection()
			pushbackSnapshot()

			if (multiLine) {
				val insert = minecraft.keyboardHandler.clipboard.replace("\t", "    ").filter { acceptsCharacter(it) }.split(NEWLINES).toMutableList()
				val actualLastSize = insert.lastOrNull()?.length ?: 0
				val line = this[cursorLine]

				if (line == null) {
					insertLine(cursorLine, insert[0])
					cursorRow = insert[0].length
				} else if (line.isEmpty()) {
					this[cursorLine] = insert[0]
					cursorRow = insert[0].length
				} else {
					if (insert.size == 1) {
						this[cursorLine] = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert[0] + line.substring(cursorRow.coerceAtMost(line.length))
					} else {
						this[cursorLine] = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert[0]
						insert[insert.size - 1] += line.substring(cursorRow.coerceAtMost(line.length))
					}

					cursorRow += insert[0].length
				}

				for (i in 1 until insert.size - 1) {
					insertLine(++cursorLine, insert[i])
					cursorRow = insert[i].length
				}

				if (insert.size >= 2) {
					val line2 = this[++cursorLine]
					val last = insert.last()

					if (line2 == null) {
						insertLine(cursorLine, last)
					} else {
						this[cursorLine] = last + line2
					}

					cursorRow = actualLastSize
				}
			} else {
				val insert = minecraft.keyboardHandler.clipboard.replace("\t", "    ").replace(NEWLINES, "").filter { acceptsCharacter(it) }
				val line = this[cursorLine]

				if (line == null) {
					insertLine(cursorLine, insert)
					cursorRow = insert.length
				} else if (line.isEmpty()) {
					this[cursorLine] = insert
					cursorRow = insert.length
				} else {
					this[cursorLine] = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert + line.substring(cursorRow.coerceAtMost(line.length))
					cursorRow += insert.length
				}
			}

			pushbackSnapshot()
			triggerChangeCallback()
			return true
		}

		if (key == InputConstants.KEY_C && minecraft.window.isCtrlDown) {
			if (selections.isEmpty()) {
				return true
			}

			val build = ArrayList<String>(selections.size)

			for ((lineNumber, selection) in selections.int2ObjectEntrySet()) {
				val line = this[lineNumber] ?: continue

				if (selection.coversEntireLine(line)) {
					build.add(line + "\n")
				} else {
					val (_, value) = selection.sub(line)
					build.add(if (selection.coversNewline(line)) value + "\n" else value)
				}
			}

			minecraft.keyboardHandler.clipboard = build.joinToString("")
			playGuiClickSound()

			return true
		}

		if (key == InputConstants.KEY_HOME) {
			if (minecraft.window.isShiftDown && cursorRow != 0) {
				val thisLine = cursorLine

				while (cursorRow > 0 && thisLine == cursorLine) {
					simulateSelectLeft(false)
				}
			} else {
				if (!minecraft.window.isShiftDown) selections.clear()
				cursorRow = 0
			}

			return true
		}

		if (key == InputConstants.KEY_END) {
			if (minecraft.window.isShiftDown) {
				val line = this[cursorLine] ?: return true
				if (cursorRow >= line.length) return true
				val thisLine = cursorLine

				while (cursorRow < line.length && thisLine == cursorLine) {
					simulateSelectRight(false)
				}
			} else {
				if (!minecraft.window.isShiftDown) selections.clear()
				cursorRow = this[cursorLine]?.length ?: 0
			}

			return true
		}

		return true
	}

	open fun acceptsCharacter(codepoint: Char, mods: Int = 0): Boolean {
		return true
	}

	override fun charTypedInternal(codepoint: Char, mods: Int): Boolean {
		if (!isActive) {
			killFocus()
			return true
		}

		if (!acceptsCharacter(codepoint, mods)) {
			playGuiClickSound()
			return true
		}

		wipeSelection()

		if (!multiLine)
			cursorLine = 0

		var line = this[cursorLine]

		if (line == null) {
			set(cursorLine, "")
			line = ""
			cursorRow = 0
		}

		if (cursorRow >= line.length)
			line += codepoint
		else
			line = line.substring(0, cursorRow) + codepoint + line.substring(cursorRow)

		pushbackSnapshotIfNoTimer()

		set(cursorLine, line)
		moveCursors(cursorLine, cursorRow, 1)
		recordHistory()
		triggerChangeCallback()

		return true
	}

	private val characterWidthCache = Char2IntOpenHashMap()

	private fun width(char: Char): Int {
		return characterWidthCache.computeIfAbsent(char, Char2IntFunction { font.width(it.toString()) })
	}

	private fun width(text: String, beginning: Int = 0, end: Int = text.length - 1): Int {
		var accumulate = 0

		for (i in beginning.coerceAtLeast(0) .. end.coerceAtMost(text.length - 1)) {
			accumulate += width(text[i])
		}

		return accumulate
	}

	override fun innerRender(graphics: GuiGraphics, mouseX: Float, mouseY: Float, partialTick: Float) {
		if (!backgroundColor.isFullyTransparent)
			graphics.renderRect(0f, 0f, width, height, color = backgroundColor)

		var topPadding = dockPadding.top

		if (multiLine) {
			val heightInLines = ((height - dockPadding.top - dockPadding.bottom) / (font.lineHeight + rowSpacing)).toInt()

			if (heightInLines > 0) {
				if (cursorLine < scrollLines) {
					scrollLines = (cursorLine - 1).coerceAtLeast(0)
				} else if (heightInLines + scrollLines < cursorLine) {
					scrollLines = (cursorLine - heightInLines).coerceIn(0, lines.size - 2)
				}
			}
		} else {
			scrollLines = 0

			topPadding += ((height - dockPadding.top - dockPadding.bottom - font.lineHeight).coerceAtLeast(0f) / 2f).roundToInt()
		}

		val selectedLine = this[cursorLine]

		if (selectedLine != null) {
			val w = width(selectedLine, end = cursorRow) - scrollPixels

			if (w < 0f) {
				scrollPixels = (scrollPixels - 30f).coerceAtLeast(0f)
			} else if (w >= width - dockPadding.right) {
				scrollPixels += 30f
			}
		} else {
			scrollPixels = 0f
		}

		val stack = graphics.pose()

		stack.pushPose()
		stack.translate(-scrollPixels, 0f, 0f)

		var y = topPadding

		if (lines.isEmpty() || lines.size == 1 && lines[0] == "") {
			graphics.drawAligned(
				font = font,
				text = placeholder,
				align = RenderGravity.TOP_LEFT,
				x = dockPadding.left,
				y = y,
				color = placeholderColor
			)
		}

		for (i in scrollLines until lines.size) {
			val line = lines[i]
			val selection = selections[i]

			graphics.drawAligned(
				font = font,
				text = line,
				align = RenderGravity.TOP_LEFT,
				x = dockPadding.left,
				y = y,
				color = textColor
			)

			if (selection != null && selection.isValid) {
				val (before, selected) = selection.sub(line)

				var x = dockPadding.left

				if (before.isNotEmpty()) {
					x += font.width(before).toFloat()
				}

				val width = if (selection.coversNewline(line) && i != lines.size - 1) this.width - x + scrollPixels else font.width(selected).toFloat()

				RenderSystem.setShader(GameRenderer::getPositionShader)
				RenderSystem.setShaderColor(cursorColor.red, cursorColor.green, cursorColor.blue, 0.4f)
				//RenderSystem.enableColorLogicOp()
				//RenderSystem.logicOp(GlStateManager.LogicOp.NOR)
				RenderSystem.disableDepthTest()
				RenderSystem.defaultBlendFunc()
				RenderSystem.enableBlend()

				val builder = tesselator.builder

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

				builder.vertex(stack.last().pose(), x, y + font.lineHeight + rowSpacing, 0f).endVertex()
				builder.vertex(stack.last().pose(), x + width, y + font.lineHeight + rowSpacing, 0f).endVertex()
				builder.vertex(stack.last().pose(), x + width, y, 0f).endVertex()
				builder.vertex(stack.last().pose(), x, y, 0f).endVertex()

				tesselator.end()

				RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f)
				//RenderSystem.disableColorLogicOp()
				RenderSystem.enableDepthTest()
			}

			y += font.lineHeight + rowSpacing

			if (y > height - dockPadding.bottom)
				break
		}

		if (isFocusedThis && milliTime % 1000L > 500L) {
			val activeLine = this[cursorLine]

			if (activeLine == null || cursorRow >= activeLine.length) {
				graphics.drawAligned(
					font = font,
					text = "_",
					align = RenderGravity.TOP_LEFT,
					x = dockPadding.left + (if (activeLine == null) 0f else font.width(activeLine).toFloat()),
					y = topPadding + (cursorLine - scrollLines) * (font.lineHeight + rowSpacing),
					color = cursorColor
				)
			} else {
				graphics.drawAligned(
					font = font,
					text = "|",
					align = RenderGravity.TOP_LEFT,
					x = dockPadding.left + font.width(activeLine.substring(0, cursorRow)).toFloat() - 1f,
					y = topPadding + (cursorLine - scrollLines) * (font.lineHeight + rowSpacing),
					color = cursorColor
				)
			}
		}

		stack.popPose()

		if (debugDraw) {
			graphics.drawAligned(
				font = font,
				text = cursorLine.toString(),
				align = RenderGravity.TOP_RIGHT,
				x = width,
				y = topPadding,
				color = cursorColor
			)

			graphics.drawAligned(
				font = font,
				text = cursorRow.toString(),
				align = RenderGravity.TOP_RIGHT,
				x = width - dockPadding.right,
				y = topPadding + font.lineHeight + rowSpacing,
				color = cursorColor
			)

			graphics.drawAligned(
				font = font,
				text = lines.size.toString(),
				align = RenderGravity.TOP_RIGHT,
				x = width - dockPadding.right,
				y = topPadding + font.lineHeight * 2f + rowSpacing * 2f,
				color = cursorColor
			)
		}

		BUFFER.endBatch()
	}

	var isSelecting = false
		protected set

	override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean {
		if (!isActive)
			return true

		if (isEverFocused())
			selections.clear()

		requestFocus()

		if (button == InputConstants.MOUSE_BUTTON_LEFT) {
			if (selections.isEmpty()) {
				val (lx, ly) = screenToLocal(x, y)
				val pos = localToText(lx, ly)
				cursorLine = pos.y
				cursorRow = pos.x
			}

			isSelecting = true
			tryToGrabMouseInput()
		}

		return true
	}

	@Suppress("name_shadowing")
	fun localToText(x: Float, y: Float): Vector2i {
		if (lines.isEmpty())
			return Vector2i(0, 0)

		var x = x - dockPadding.left
		var y = y - dockPadding.top

		if (y > height - dockPadding.bottom)
			y = height - dockPadding.bottom

		if (x > width - dockPadding.right)
			x = width - dockPadding.right

		x += scrollPixels

		val line = (scrollLines + (y / (font.lineHeight + rowSpacing)).toInt()).coerceIn(0, lines.size - 1)
		val sLine = this[line] ?: return Vector2i(0, line)

		if (x <= 0f)
			return Vector2i(0, line)
		else if (x >= width(sLine))
			return Vector2i(sLine.length, line)

		var accumulatedWidth = 0f

		for ((i, char) in sLine.withIndex()) {
			val width = width(char)

			if (x in accumulatedWidth .. accumulatedWidth + width) {
				if (x - accumulatedWidth < width / 2)
					return Vector2i(i, line)
				else
					return Vector2i(i + 1, line)
			} else {
				accumulatedWidth += width
			}
		}

		return Vector2i(sLine.length, line)
	}

	override fun mouseReleasedInner(x: Double, y: Double, button: Int): Boolean {
		if (button == InputConstants.MOUSE_BUTTON_LEFT) {
			if (isSelecting) {
				grabMouseInput = false
				isSelecting = false
			}
		}

		return true
	}

	override fun mouseDraggedInner(x: Double, y: Double, button: Int, xDelta: Double, yDelta: Double): Boolean {
		if (button != InputConstants.MOUSE_BUTTON_LEFT || !isSelecting)
			return true

		val (lx, ly) = screenToLocal(x, y)
		val pos = localToText(lx, ly)
		val cursorLine = pos.y
		val cursorCharacter = pos.x

		while (cursorLine > this.cursorLine && simulateSelectionDown()) {}
		while (cursorLine < this.cursorLine && simulateSelectionUp()) {}

		var lastCursorCharacter = this.cursorRow + 1

		while (cursorCharacter < this.cursorRow && lastCursorCharacter != this.cursorRow) {
			lastCursorCharacter = this.cursorRow
			simulateSelectLeft(false)
		}

		while (cursorCharacter > this.cursorRow && lastCursorCharacter != this.cursorRow) {
			lastCursorCharacter = this.cursorRow
			simulateSelectRight(false)
		}

		return true
	}

	protected open fun onTextChanged(old: String, new: String) {
		changeCallback?.invoke(old, new)
	}

	protected var changeCallback: ((old: String, new: String) -> Unit)? = null

	fun onTextChange(callback: (old: String, new: String) -> Unit) {
		changeCallback = callback
	}

	private enum class CharType {
		SPACES {
			override fun contains(input: Char): Boolean {
				return _SPACES.contains(input)
			}

			override fun canJumpInto(category: CharType): Boolean {
				return true
			}
		},
		CONTROL {
			override fun contains(input: Char): Boolean {
				return _CONTROL.contains(input)
			}

			override fun canJumpInto(category: CharType): Boolean {
				return category == CONTROL
			}
		},
		OTHER {
			override fun contains(input: Char): Boolean {
				return !_SPACES.contains(input) && !_CONTROL.contains(input)
			}

			override fun canJumpInto(category: CharType): Boolean {
				return category == OTHER
			}
		};

		abstract fun contains(input: Char): Boolean
		abstract fun canJumpInto(category: CharType): Boolean

		companion object {
			private val values = values()

			operator fun get(input: Char): CharType {
				for (value in values) {
					if (value.contains(input)) {
						return value
					}
				}

				throw NoSuchElementException("for $input")
			}
		}
	}

	companion object {
		val NEWLINES = Regex("\r?\n")
		private val BUFFER = DynamicBufferSource()

		private val _SPACES = CharOpenHashSet().also {
			for (char in " \t\n\r\u0000")
				it.add(char)
		}

		private val _CONTROL = CharOpenHashSet().also {
			for (char in "!@\"'#$%^&?*()~`[]/\\;:|<>.,")
				it.add(char)
		}

		private fun greedyAdvanceLeft(input: String, position: Int): Int {
			if (position <= 1)
				return -1

			@Suppress("name_shadowing")
			var position = position
			var type = CharType[input[--position]]

			while (position > 0) {
				val newType = CharType[input[position]]

				if (type.canJumpInto(newType)) {
					position--
					type = newType
				} else {
					return position + 1
				}
			}

			return position
		}

		private fun greedyAdvanceRight(input: String, position: Int): Int {
			@Suppress("name_shadowing")
			var position = position

			if (position < 0)
				position = 0
			else if (position >= input.length)
				return position + 1

			var type = CharType[input[position]]

			while (position < input.length) {
				val newType = CharType[input[position]]

				if (type.canJumpInto(newType)) {
					position++
					type = newType
				} else {
					return position
				}
			}

			return position
		}
	}
}
