package ru.dbotthepony.kstarbound.defs.`object`

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.clear
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.render.RenderLayer
import ru.dbotthepony.kstarbound.defs.Drawable
import ru.dbotthepony.kstarbound.defs.JsonReference
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.json.listAdapter
import ru.dbotthepony.kstarbound.json.setAdapter
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World
import kotlin.math.PI

@JsonAdapter(ObjectOrientation.Adapter::class)
data class ObjectOrientation(
	val json: JsonObject,
	val flipImages: Boolean = false,
	val drawables: ImmutableList<Drawable>,
	val renderLayer: RenderLayer.Point,
	val imagePosition: Vector2f,
	val frames: Int,
	val animationCycle: Double,
	// world tiles this object occupy while in this orientation
	val occupySpaces: ImmutableSet<Vector2i>,
	val boundingBox: AABBi,
	val metaBoundBox: AABB?,
	val anchors: ImmutableSet<Anchor>,
	val anchorAny: Boolean,
	val directionAffinity: Direction?,
	val materialSpaces: ImmutableList<Pair<Vector2i, Registry.Ref<TileDefinition>>>,
	val interactiveSpaces: ImmutableSet<Vector2i>,
	val lightPosition: Vector2i,
	val beamAngle: Double,
	val statusEffectArea: Vector2d?,
	val touchDamage: JsonReference.Object?,
	val particleEmitters: ArrayList<ParticleEmissionEntry>,
) {
	fun placementValid(world: World<*, *>, position: Vector2i, ignoreProtectedDungeons: Boolean = false): Boolean {
		if (occupySpaces.isEmpty())
			return true

		return occupySpaces.all {
			val cell = world.chunkMap.getCell(it + position)
			//if (!cell.foreground.material.isEmptyTile) println("not empty tile: ${it + position}, space $it, pos $position")
			//if (cell.dungeonId in world.protectedDungeonIDs) println("position is protected: ${it + position}")
			cell.foreground.material.isEmptyTile && (ignoreProtectedDungeons || cell.dungeonId !in world.protectedDungeonIDs)
		}
	}

	fun anchorsValid(world: World<*, *>, position: Vector2i): Boolean {
		if (anchors.isEmpty())
			return true

		var anyValid = false

		for (anchor in anchors) {
			val isValid = anchor.isValidPlacement(world, position)

			if (isValid)
				anyValid = true
			else if (!anchorAny) {
				// println("anchor $anchor reported false for $position ${world.chunkMap.getCell(position + anchor.position)}")
				return false
			}
		}

		return anyValid
	}

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

		fun preprocess(json: JsonArray): JsonArray {
			val actual = ArrayList<JsonObject>()

			for (elem in json) {
				val obj = elem.asJsonObject

				if ("dualImage" in obj) {
					var copy = obj.deepCopy()
					copy["image"] = obj["dualImage"]!!
					copy["flipImages"] = true
					copy["direction"] = "left"
					actual.add(copy)

					copy = obj.deepCopy()
					copy["image"] = obj["dualImage"]!!
					copy["flipImages"] = false
					copy["direction"] = "right"
					actual.add(copy)
				} else if ("leftImage" in obj) {
					require("rightImage" in obj) { "Provided leftImage, but there is no rightImage!" }

					var copy = obj.deepCopy()
					copy["image"] = obj["leftImage"]!!
					copy["direction"] = "left"
					actual.add(copy)

					copy = obj.deepCopy()
					copy["image"] = obj["rightImage"]!!
					copy["direction"] = "right"
					actual.add(copy)
				} else {
					actual.add(obj)
				}
			}

			json.clear()
			actual.forEach { json.add(it) }
			return json
		}
	}

	class Adapter(gson: Gson) : TypeAdapter<ObjectOrientation>() {
		private val vectors = gson.getAdapter(Vector2f::class.java)
		private val vectorsi = gson.getAdapter(Vector2i::class.java)
		private val vectorsd = gson.getAdapter(Vector2d::class.java)
		private val drawables = gson.getAdapter(Drawable::class.java)
		private val aabbs = gson.getAdapter(AABB::class.java)
		private val objectRefs = gson.getAdapter(JsonReference.Object::class.java)
		private val emitter = gson.getAdapter(ParticleEmissionEntry::class.java)
		private val emitters = gson.listAdapter<ParticleEmissionEntry>()
		private val spaces = gson.setAdapter<Vector2i>()
		private val materialSpaces = gson.getAdapter(TypeToken.getParameterized(ImmutableList::class.java, TypeToken.getParameterized(Pair::class.java, Vector2i::class.java, String::class.java).type)) as TypeAdapter<ImmutableList<Pair<Vector2i, String>>>

		override fun write(out: JsonWriter, value: ObjectOrientation?) {
			if (value == null) {
				out.nullValue()
				return
			} else {
				TODO()
			}
		}

		override fun read(`in`: JsonReader): ObjectOrientation? {
			if (`in`.consumeNull())
				return null

			val obj = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
			val drawables = ArrayList<Drawable>()
			val flipImages = obj.get("flipImages", false)
			val renderLayer = RenderLayer.parse(obj.get("renderLayer", "Object"))

			if ("imageLayers" in obj) {
				for (value in obj["imageLayers"].asJsonArray) {
					var result = this.drawables.fromJsonTree(value)

					if (flipImages) {
						result = result.flop()
					}

					drawables.add(result)
				}
			} else {
				var result = this.drawables.fromJsonTree(obj)

				if (flipImages) {
					result = result.flop()
				}

				drawables.add(result)
			}

			val imagePosition = (obj["imagePosition"]?.let { vectors.fromJsonTree(it) } ?: Vector2f.ZERO) / PIXELS_IN_STARBOUND_UNITf
			val imagePositionI = (obj["imagePosition"]?.let { vectorsi.fromJsonTree(it) } ?: Vector2i.ZERO)
			val frames = obj.get("frames", 1)
			val animationCycle = obj.get("animationCycle", 1.0)
			var occupySpaces = obj["spaces"]?.let { spaces.fromJsonTree(it) } ?: ImmutableSet.of(Vector2i.ZERO)

			if ("spaceScan" in obj) {
				occupySpaces = ImmutableSet.of()

				try {
					for (drawable in drawables) {
						if (drawable is Drawable.Image) {
							val bound = drawable.path.with { "default" }
							val sprite = bound.sprite

							if (sprite != null) {
								val new = ImmutableSet.Builder<Vector2i>()
								new.addAll(occupySpaces)
								new.addAll(sprite.worldSpaces(imagePositionI, obj["spaceScan"].asDouble, flipImages))
								occupySpaces = new.build()
							} else {
								LOGGER.error("Unable to space scan image, not a valid sprite reference: $bound")
							}
						}
					}
				} catch (err: Throwable) {
					throw JsonSyntaxException("Unable to space scan image", err)
				}
			}

			val minX: Int
			val minY: Int
			val maxX: Int
			val maxY: Int

			if (occupySpaces.isNotEmpty()) {
				minX = occupySpaces.minOf { it.x }
				minY = occupySpaces.minOf { it.y }
				maxX = occupySpaces.maxOf { it.x }
				maxY = occupySpaces.maxOf { it.y }
			} else {
				minX = 0
				minY = 0
				maxX = 0
				maxY = 0
			}

			val boundingBox = AABBi(Vector2i(minX, minY), Vector2i(maxX, maxY))

			val metaBoundBox = obj["metaBoundBox"]?.let { aabbs.fromJsonTree(it) }
			val requireTilledAnchors = obj.get("requireTilledAnchors", false)
			val requireSoilAnchors = obj.get("requireSoilAnchors", false)
			val anchorMaterial = obj["anchorMaterial"]?.asString?.let { Registries.tiles.ref(it) }
			val anchors = ImmutableSet.Builder<Anchor>()

			for (v in obj.get("anchors", JsonArray())) {
				when (v.asString.lowercase()) {
					"left" -> occupySpaces.stream().filter { it.x == minX }.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
					"right" -> occupySpaces.stream().filter { it.x == maxX }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
					"top" -> occupySpaces.stream().filter { it.y == maxY }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
					"bottom" -> occupySpaces.stream().filter { it.y == minY }.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
					"background" -> occupySpaces.forEach { anchors.add(Anchor(true, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
					else -> throw JsonSyntaxException("Unknown anchor type $v")
				}
			}

			for (v in obj.get("bgAnchors", JsonArray()))
				anchors.add(Anchor(true, vectorsi.fromJsonTree(v), requireTilledAnchors, requireSoilAnchors, anchorMaterial))

			for (v in obj.get("fgAnchors", JsonArray()))
				anchors.add(Anchor(false, vectorsi.fromJsonTree(v), requireTilledAnchors, requireSoilAnchors, anchorMaterial))

			val anchorAny = obj["anchorAny"]?.asBoolean ?: false
			val directionAffinity = obj["direction"]?.asString?.uppercase()?.let { Direction.valueOf(it) }
			val materialSpaces: ImmutableList<Pair<Vector2i, String>>

			if ("materialSpaces" in obj) {
				materialSpaces = this.materialSpaces.fromJsonTree(obj["materialSpaces"])
			} else {
				val collisionSpaces = obj["collisionSpaces"]?.let { this.spaces.fromJsonTree(it) } ?: occupySpaces
				val builder = ImmutableList.Builder<Pair<Vector2i, String>>()

				when (val collisionType = obj.get("collision", "none").lowercase()) {
					"solid" -> collisionSpaces.forEach { builder.add(it to BuiltinMetaMaterials.OBJECT_SOLID.key) }
					"platform" -> collisionSpaces.forEach { if (it.y == boundingBox.maxs.y) builder.add(it to BuiltinMetaMaterials.OBJECT_PLATFORM.key) }
					"none" -> {}
					else -> throw JsonSyntaxException("Unknown collision type $collisionType")
				}

				materialSpaces = builder.build()
			}

			val interactiveSpaces = obj["interactiveSpaces"]?.let { this.spaces.fromJsonTree(it) } ?: occupySpaces
			val lightPosition = obj["lightPosition"]?.let { vectorsi.fromJsonTree(it) } ?: Vector2i.ZERO
			val beamAngle = obj.get("beamAngle", 0.0) / 180.0 * PI
			val statusEffectArea = obj["statusEffectArea"]?.let { vectorsd.fromJsonTree(it) }
			val touchDamage = obj["touchDamage"]?.let { objectRefs.fromJsonTree(it) }

			val emitters = ArrayList<ParticleEmissionEntry>()

			if ("particleEmitter" in obj) {
				emitters.add(this.emitter.fromJsonTree(obj["particleEmitter"]))
			}

			if ("particleEmitters" in obj) {
				emitters.addAll(this.emitters.fromJsonTree(obj["particleEmitters"]))
			}

			return ObjectOrientation(
				obj,
				flipImages = flipImages,
				drawables = ImmutableList.copyOf(drawables),
				renderLayer = renderLayer,
				imagePosition = imagePosition,
				frames = frames,
				animationCycle = animationCycle,
				occupySpaces = occupySpaces,
				boundingBox = boundingBox,
				metaBoundBox = metaBoundBox,
				anchors = anchors.build(),
				anchorAny = anchorAny,
				directionAffinity = directionAffinity,
				materialSpaces = materialSpaces.stream().map { it.first to Registries.tiles.ref(it.second) }.collect(ImmutableList.toImmutableList()),
				interactiveSpaces = interactiveSpaces,
				lightPosition = lightPosition,
				beamAngle = beamAngle,
				statusEffectArea = statusEffectArea,
				touchDamage = touchDamage,
				particleEmitters = emitters,
			)
		}
	}
}
