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.JsonSyntaxException
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kstarbound.IStarboundFile
import ru.dbotthepony.kstarbound.Starbound

enum class JsonPatch(val key: String) {
	TEST("test") {
		override fun apply(base: JsonElement, data: JsonObject): JsonElement {
			val path = JsonPath.pointer(data.get("path").asString)
			val value = data["value"] ?: JsonNull.INSTANCE
			val inverse = data.get("inverse", false)

			val get = path.find(base) ?: JsonNull.INSTANCE

			if (inverse && get == value) {
				throw IllegalStateException("Expected $path to not contain $value")
			} else if (!inverse && get != value && value != JsonNull.INSTANCE) {
				var text = get.toString()

				if (text.length > 40) {
					text = text.substring(0, 40) + "..."
				}

				throw IllegalStateException("Expected $path to contain $value, but found $text")
			}

			return base
		}
	},
	REMOVE("remove") {
		override fun apply(base: JsonElement, data: JsonObject): JsonElement {
			return JsonPath.pointer(data.get("path").asString).remove(base).first
		}
	},
	ADD("add") {
		override fun apply(base: JsonElement, data: JsonObject): JsonElement {
			val value = data["value"] ?: throw JsonSyntaxException("Missing 'value' to 'add' operation")
			return JsonPath.pointer(data.get("path").asString).add(base, value)
		}
	},
	REPLACE("replace") {
		override fun apply(base: JsonElement, data: JsonObject): JsonElement {
			val value = data["value"] ?: throw JsonSyntaxException("Missing 'value' to 'add' operation")
			val path = JsonPath.pointer(data.get("path").asString)
			return path.add(path.remove(base).first, value)
		}
	},
	MOVE("move") {
		override fun apply(base: JsonElement, data: JsonObject): JsonElement {
			val from = JsonPath.pointer(data.get("from").asString)
			val path = JsonPath.pointer(data.get("path").asString)
			val (newBase, removed) = from.remove(base)
			return path.add(newBase, removed)
		}
	},
	COPY("copy") {
		override fun apply(base: JsonElement, data: JsonObject): JsonElement {
			val from = JsonPath.pointer(data.get("from").asString)
			val path = JsonPath.pointer(data.get("path").asString)
			return path.add(base, from.get(base))
		}
	};

	abstract fun apply(base: JsonElement, data: JsonObject): JsonElement

	companion object {
		private val LOGGER = LogManager.getLogger()

		fun apply(base: JsonElement, data: JsonObject): JsonElement {
			val op = data["op"]?.asString ?: throw JsonSyntaxException("Missing 'op' in json patch operation")
			val operation = entries.firstOrNull { it.key == op } ?: throw JsonSyntaxException("Unknown patch operation $op!")
			return operation.apply(base, data)
		}

		@Suppress("NAME_SHADOWING")
		fun apply(base: JsonElement, data: JsonArray, source: IStarboundFile?): JsonElement {
			// original engine decides what to do based on first element encountered
			// in patch array... let's not.
			var base = base

			for ((i, element) in data.withIndex()) {
				if (element is JsonObject) {
					try {
						base = apply(base, element)
					} catch (err: Throwable) {
						LOGGER.error("Error while applying JSON patch from $source at index $i: ${err.message}")
					}
				} else if (element is JsonArray) {
					for ((i2, sub) in element.withIndex()) {
						try {
							if (sub !is JsonObject)
								throw JsonSyntaxException("Expected JSON Object as patch data, got $sub")

							base = apply(base, sub)
						} catch (err: Throwable) {
							LOGGER.error("Error while applying JSON patch from $source at index $i -> $i2: ${err.message}")
							break
						}
					}
				} else {
					LOGGER.error("Unknown data in $source at index $i")
				}
			}

			return base
		}

		@Suppress("NAME_SHADOWING")
		fun apply(base: JsonElement, source: Collection<IStarboundFile>?): JsonElement {
			source ?: return base
			var base = base

			for (patch in source) {
				val read = Starbound.ELEMENTS_ADAPTER.read(patch.jsonReader())

				if (read !is JsonArray) {
					LOGGER.error("$patch root element is not an array")
				} else {
					base = apply(base, read, patch)
				}
			}

			return base
		}
	}
}
