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.JsonPrimitive
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import java.io.Reader

// JsonTreeReader which allows to retrieve next json element instead of "reading" it
//  through JsonReader methods
class FastJsonTreeReader(top: JsonElement) : JsonReader(unreadable) {
	private var stack = arrayOfNulls<Any>(32)
	private var pathNames = arrayOfNulls<String>(32)
	private var pathIndices = IntArray(32)
	private var stackSize = 0

	private fun push(element: Any?) {
		if (stackSize >= stack.size) {
			stack = stack.copyOf(stack.size * 2)
			pathNames = pathNames.copyOf(stack.size)
			pathIndices = pathIndices.copyOf(stack.size)
		}

		stack[stackSize++] = element
	}

	private fun pop(): Any? {
		val result = stack[--stackSize]
		stack[stackSize] = null
		return result
	}

	private fun popWithShift(): Any? {
		val result = pop()
		if (stackSize > 0) pathIndices[stackSize - 1]++
		return result
	}

	private fun peekStack() = stack[stackSize - 1]

	init {
		push(top)
	}

	override fun beginArray() {
		expect(JsonToken.BEGIN_ARRAY)
		push((peekStack() as JsonArray).iterator())
		pathIndices[stackSize - 1] = 0
	}

	override fun endArray() {
		expect(JsonToken.END_ARRAY)
		pop() // empty iterator
		pop() // array

		if (stackSize > 0) {
			pathIndices[stackSize - 1]++
		}
	}

	override fun beginObject() {
		expect(JsonToken.BEGIN_OBJECT)
		push((peekStack() as JsonObject).entrySet().iterator())
	}

	override fun endObject() {
		expect(JsonToken.END_OBJECT)
		pop() // empty iterator
		pop() // object

		if (stackSize > 0) {
			pathIndices[stackSize - 1]++
		}
	}

	fun popObject(): JsonObject {
		expect(JsonToken.BEGIN_OBJECT)
		return popWithShift() as JsonObject
	}

	fun popArray(): JsonArray {
		expect(JsonToken.BEGIN_ARRAY)
		return popWithShift() as JsonArray
	}

	fun popJsonElement(): JsonElement {
		if (stackSize == 0)
			throw IllegalStateException("Reader is empty")

		if (peekStack() is JsonElement) {
			return popWithShift() as JsonElement
		}

		throw IllegalStateException("Not a json element${locationString()}")
	}

	override fun nextName(): String {
		expect(JsonToken.NAME)
		val (key, value) = (peekStack() as Iterator<Map.Entry<String, JsonElement>>).next()
		pathNames[stackSize - 1] = key
		push(value)
		return key
	}

	override fun nextString(): String {
		val peek = peek()
		check(peek === JsonToken.STRING || peek === JsonToken.NUMBER) { "Expected STRING but was $peek${locationString()}" }
		return (popWithShift() as JsonPrimitive).asString
	}

	override fun nextBoolean(): Boolean {
		expect(JsonToken.BOOLEAN)
		return (popWithShift() as JsonPrimitive).asBoolean
	}

	override fun nextNull() {
		expect(JsonToken.NULL)
		popWithShift()
	}

	override fun nextDouble(): Double {
		val peek = peek()
		check(peek === JsonToken.STRING || peek === JsonToken.NUMBER) { "Expected NUMBER but was $peek${locationString()}" }

		val result = (popWithShift() as JsonPrimitive).asDouble

		if (!isLenient && (result.isNaN() || result.isInfinite())) {
			throw NumberFormatException("JSON forbids NaN and infinities: $result")
		}

		return result
	}

	override fun nextLong(): Long {
		val peek = peek()
		check(peek === JsonToken.STRING || peek === JsonToken.NUMBER) { "Expected NUMBER but was $peek${locationString()}" }
		return (popWithShift() as JsonPrimitive).asLong
	}

	override fun nextInt(): Int {
		val peek = peek()
		check(peek === JsonToken.STRING || peek === JsonToken.NUMBER) { "Expected NUMBER but was $peek${locationString()}" }
		return (popWithShift() as JsonPrimitive).asInt
	}

	override fun skipValue() {
		if (peek() === JsonToken.NAME) {
			nextName()
			pathNames[stackSize - 2] = "null"
		} else {
			pop()
			if (stackSize > 0) pathNames[stackSize - 1] = "null"
		}

		if (stackSize > 0) pathIndices[stackSize - 1]++
	}

	override fun close() {
		// do nothing
	}

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

	override fun peek(): JsonToken {
		if (stackSize == 0)
			return JsonToken.END_DOCUMENT

		val peek = stack[stackSize - 1]

		return when (peek) {
			is JsonObject -> JsonToken.BEGIN_OBJECT
			is JsonArray -> JsonToken.BEGIN_ARRAY
			is JsonPrimitive -> {
				if (peek.isString) {
					return JsonToken.STRING
				} else if (peek.isBoolean) {
					return JsonToken.BOOLEAN
				} else if (peek.isNumber) {
					return JsonToken.NUMBER
				} else {
					throw RuntimeException("unreachable code")
				}
			}

			JsonNull.INSTANCE -> JsonToken.NULL

			is Iterator<*> -> {
				val isObject = stack[stackSize - 2] is JsonObject

				if (peek.hasNext()) {
					if (isObject) {
						return JsonToken.NAME
					} else {
						push(peek.next())
						return peek()
					}
				} else {
					if (isObject) JsonToken.END_OBJECT else JsonToken.END_ARRAY
				}
			}

			else -> throw RuntimeException()
		}
	}

	override fun getPath(): String {
		val result = StringBuilder().append('$')
		var i = 0

		while (i < stack.size) {
			if (stack[i] is JsonArray) {
				if (++i < stack.size && stack[i] is Iterator<*>) {
					result.append('[').append(pathIndices[i]).append(']')
				}
			} else if (stack[i] is JsonObject) {
				if (++i < stack.size && stack[i] is Iterator<*>) {
					result.append('.')
					if (pathNames[i] != null) {
						result.append(pathNames[i])
					}
				}
			}

			i++
		}

		return result.toString()
	}

	private fun locationString(): String {
		return " at path $path"
	}

	private fun expect(token: JsonToken) {
		check(peek() === token) { "Expected $token but was ${peek()}${locationString()}" }
	}

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

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