package ru.dbotthepony.kstarbound.json

import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.readSignedVarLong
import ru.dbotthepony.kommons.io.readString
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kstarbound.Starbound
import java.io.DataInputStream
import java.io.EOFException
import java.io.InputStream
import java.io.Reader
import java.util.LinkedList

/**
 * Позволяет читать двоичный JSON прямиком в [JsonElement]
 */
fun DataInputStream.readJsonElement(): JsonElement {
	return when (val id = readUnsignedByte()) {
		BinaryJsonReader.TYPE_INVALID -> throw JsonParseException("Tried to read TYPE_INVALID from stream")
		BinaryJsonReader.TYPE_NULL -> JsonNull.INSTANCE
		BinaryJsonReader.TYPE_DOUBLE -> JsonPrimitive(readDouble())
		BinaryJsonReader.TYPE_BOOLEAN -> InternedJsonElementAdapter.of(readBoolean())
		BinaryJsonReader.TYPE_INT -> JsonPrimitive(readSignedVarLong())
		BinaryJsonReader.TYPE_STRING -> JsonPrimitive(Starbound.STRINGS.intern(readBinaryString()))
		BinaryJsonReader.TYPE_ARRAY -> readJsonArray()
		BinaryJsonReader.TYPE_OBJECT -> readJsonObject()
		else -> throw JsonParseException("Unknown element type $id")
	}
}

/**
 * Позволяет читать двоичный JSON объект прямиком в [JsonObject]
 */
fun DataInputStream.readJsonObject(): JsonObject {
	val values = readVarInt()
	if (values == 0) return JsonObject()
	if (values < 0) throw JsonSyntaxException("Tried to read json object with $values elements in it")

	val build = JsonObject()

	for (i in 0 until values) {
		val key = try {
			Starbound.STRINGS.intern(readBinaryString())
		} catch(err: Throwable) {
			throw JsonSyntaxException("Reading json object at $i", err)
		}

		try {
			build.add(key, readJsonElement())
		} catch(err: Throwable) {
			throw JsonSyntaxException("Reading json object at $i with name $key", err)
		}
	}

	return build
}

/**
 * Позволяет читать двоичный JSON массив прямиком в [JsonArray]
 */
fun DataInputStream.readJsonArray(): JsonArray {
	val values = readVarInt()

	if (values == 0) return JsonArray()
	if (values < 0) throw JsonSyntaxException("Tried to read json array with $values elements in it")

	val build = JsonArray(values)
	for (i in 0 until values) build.add(readJsonElement())
	return build
}

class BinaryJsonReader(private val stream: DataInputStream) : JsonReader(unreadable) {
	constructor(stream: InputStream) : this(DataInputStream(stream))

	private var needToReadNext = true

	private fun popstack(check: NothingReader) {
		val popped = stack.removeLast()
		check(popped == check) { "$popped != $check" }
		needToReadNext = true
	}

	private fun actuallyReadNext(expectingValidValue: Boolean) {
		when (val id = stream.read()) {
			TYPE_NULL -> stack.addLast(nullReader)
			TYPE_DOUBLE -> stack.addLast(DoubleReader(stream.readDouble()))
			TYPE_BOOLEAN -> stack.addLast(if (stream.readBoolean()) trueReader else falseReader)
			TYPE_INT -> stack.addLast(LongReader(stream.readSignedVarLong()))
			TYPE_STRING -> stack.addLast(StringReader(stream.readVarInt()))
			TYPE_OBJECT -> stack.addLast(ObjectReader(stream.readVarInt()))
			TYPE_ARRAY -> stack.addLast(ArrayReader(stream.readVarInt()))
			-1 -> { if (expectingValidValue) throw EOFException("Unexpected end of stream while expecting binary json element") }
			else -> throw IllegalArgumentException("Invalid type ID: $id")
		}
	}

	private fun readNext() {
		if (!needToReadNext)
			return

		needToReadNext = false
		val last = stack.lastOrNull()

		if (last is ObjectReader) {
			if (last.readPairs == last.pairs) {
				stack.removeLast()
				stack.addLast(endObject)
				return
			}

			if (last.readingName) {
				last.readingName = false
				stack.addLast(NameReader(stream.readBinaryString()))
			} else {
				last.readingName = true
				last.readPairs++
				actuallyReadNext(true)
			}
		} else if (last is ArrayReader) {
			if (last.readValues == last.size) {
				stack.removeLast()
				stack.addLast(endArray)
				return
			}

			last.readValues++
			actuallyReadNext(true)
		} else {
			actuallyReadNext(false)
		}
	}

	override fun peek(): JsonToken {
		readNext()
		return (stack.lastOrNull() ?: endReader).peek()
	}

	override fun hasNext(): Boolean {
		return peek() != JsonToken.END_OBJECT && peek() != JsonToken.END_ARRAY
	}

	private abstract inner class NothingReader(val name: String, val token: JsonToken) {
		open fun beginArray(): Unit = throw JsonSyntaxException("Expected BEGIN_ARRAY, got $name")
		open fun endArray(): Unit = throw JsonSyntaxException("Expected END_ARRAY, got $name")
		open fun beginObject(): Unit = throw JsonSyntaxException("Expected BEGIN_OBJECT, got $name")
		open fun endObject(): Unit = throw JsonSyntaxException("Expected END_OBJECT, got $name")
		open fun nextName(): String = throw JsonSyntaxException("Expected a name, got $name")
		open fun nextString(): String = throw JsonSyntaxException("Expected a string, got $name")
		open fun nextBoolean(): Boolean = throw JsonSyntaxException("Expected a boolean, got $name")
		open fun nextNull(): Unit = throw JsonSyntaxException("Expected a null, got $name")
		open fun nextDouble(): Double = throw JsonSyntaxException("Expected a double, got $name")
		open fun nextLong(): Long = throw JsonSyntaxException("Expected a long, got $name")
		open fun nextInt(): Int = throw JsonSyntaxException("Expected an int, got $name")

		open fun skipValue() {
			popstack(this)
		}

		open fun peek(): JsonToken = token

		override fun toString(): String {
			return "ReaderShard[$name, $token]"
		}
	}

	private inner class NullReader : NothingReader("null", JsonToken.NULL) {
		override fun nextNull() { popstack(this) }
	}

	private inner class TrueReader : NothingReader("boolean", JsonToken.BOOLEAN) {
		override fun nextBoolean(): Boolean { popstack(this); return true }
	}

	private inner class FalseReader : NothingReader("boolean", JsonToken.BOOLEAN) {
		override fun nextBoolean(): Boolean { popstack(this); return false }
	}

	private inner class EndReader : NothingReader("end_document", JsonToken.END_DOCUMENT) {
		override fun skipValue() {
			check(stack.size == 0)
		}
	}

	private val nullReader = NullReader()
	private val trueReader = TrueReader()
	private val falseReader = FalseReader()
	private val endReader = EndReader()

	private inner class LongReader(val value: Long) : NothingReader("long", JsonToken.NUMBER) {
		override fun nextLong(): Long {
			popstack(this)
			return value
		}

		override fun nextDouble() = nextLong().toDouble()
		override fun nextInt() = nextLong().toInt()
		override fun nextString() = nextLong().toString()
	}

	private inner class DoubleReader(val value: Double) : NothingReader("double", JsonToken.NUMBER) {
		override fun nextDouble(): Double {
			popstack(this)
			return value
		}

		override fun nextInt() = nextDouble().toInt()
		override fun nextLong() = nextDouble().toLong()
		override fun nextString() = nextDouble().toString()
	}

	private inner class StringReader(val length: Int) : NothingReader("string", JsonToken.STRING) {
		private val value by lazy(LazyThreadSafetyMode.NONE) { Starbound.STRINGS.intern(stream.readString(length)) }

		override fun skipValue() {
			popstack(this)
			stream.skipNBytes(length.toLong())
		}

		override fun nextString(): String {
			popstack(this)
			return value
		}

		override fun nextInt(): Int {
			try {
				return value.toInt().also { popstack(this) }
			} catch(_: NumberFormatException) {
				try {
					return value.toDouble().toInt().also { popstack(this) }
				} catch (_: NumberFormatException) {

				}
			}

			throw JsonSyntaxException("Expected an int, got string near ${this@BinaryJsonReader.path}")
		}

		override fun nextLong(): Long {
			try {
				return value.toLong().also { popstack(this) }
			} catch(_: NumberFormatException) {
				try {
					return value.toDouble().toLong().also { popstack(this) }
				} catch (_: NumberFormatException) {

				}
			}

			throw JsonSyntaxException("Expected an long, got string near ${this@BinaryJsonReader.path}")
		}

		override fun nextDouble(): Double {
			try {
				return value.toDouble().also { popstack(this) }
			} catch (_: NumberFormatException) {

			}

			throw JsonSyntaxException("Expected an long, got string near ${this@BinaryJsonReader.path}")
		}
	}

	private inner class NameReader(val value: String) : NothingReader("name", JsonToken.NAME) {
		override fun nextName(): String {
			popstack(this)
			return value
		}
	}

	private inner class ObjectReader(val pairs: Int) : NothingReader("object", JsonToken.BEGIN_OBJECT) {
		var readingName = true
		var readPairs = 0

		var consumed = false

		override fun beginObject() {
			check(!consumed) { "Already called beginObject on this object" }
			check(stack.last() === this)
			consumed = true
			needToReadNext = true
		}
	}

	private inner class EndObject : NothingReader("end object", JsonToken.END_OBJECT) {
		override fun endObject() {
			popstack(this)
		}
	}

	private val endObject = EndObject()

	private inner class ArrayReader(val size: Int) : NothingReader("array", JsonToken.BEGIN_ARRAY) {
		var readValues = 0
		var consumed = false

		override fun beginArray() {
			check(!consumed) { "Already called beginArray on this array" }
			check(stack.last() === this)
			consumed = true
			needToReadNext = true
		}
	}

	private inner class EndArray : NothingReader("end array", JsonToken.END_ARRAY) {
		override fun endArray() {
			popstack(this)
		}
	}

	override fun close() {
		stack.clear()
		stream.close()
	}

	override fun getPath(): String {
		return "<binary json>"
	}

	private val endArray = EndArray()

	private val stack = LinkedList<NothingReader>()

	override fun nextDouble(): Double {
		readNext()
		return (stack.lastOrNull() ?: endReader).nextDouble()
	}

	override fun nextLong(): Long {
		readNext()
		return (stack.lastOrNull() ?: endReader).nextLong()
	}

	override fun nextInt(): Int {
		readNext()
		return (stack.lastOrNull() ?: endReader).nextInt()
	}

	override fun nextNull() {
		readNext()
		return (stack.lastOrNull() ?: endReader).nextNull()
	}

	override fun beginObject() {
		readNext()
		return (stack.lastOrNull() ?: endReader).beginObject()
	}

	override fun endObject() {
		readNext()
		return (stack.lastOrNull() ?: endReader).endObject()
	}

	override fun beginArray() {
		readNext()
		return (stack.lastOrNull() ?: endReader).beginArray()
	}

	override fun endArray() {
		readNext()
		return (stack.lastOrNull() ?: endReader).endArray()
	}

	override fun nextName(): String {
		readNext()
		return (stack.lastOrNull() ?: endReader).nextName()
	}

	override fun nextString(): String {
		readNext()
		return (stack.lastOrNull() ?: endReader).nextString()
	}

	override fun nextBoolean(): Boolean {
		readNext()
		return (stack.lastOrNull() ?: endReader).nextBoolean()
	}

	override fun skipValue() {
		readNext()
		return (stack.lastOrNull() ?: endReader).skipValue()
	}

	companion object {
		const val TYPE_INVALID = 0x00
		const val TYPE_NULL = 0x01
		const val TYPE_DOUBLE = 0x02
		const val TYPE_BOOLEAN = 0x03

		/**
		 * На самом деле, variable int
		 */
		const val TYPE_INT = 0x04
		const val TYPE_STRING = 0x05
		const val TYPE_ARRAY = 0x06
		const val TYPE_OBJECT = 0x07

		private val unreadable = object : Reader() {
			override fun read(cbuf: CharArray, off: Int, len: Int): Int {
				throw AssertionError()
			}

			override fun close() {
				throw AssertionError()
			}
		}
	}
}
