package ru.dbotthepony.kstarbound.world.entities.tile

import com.google.common.collect.ImmutableSet
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.image.Image
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.io.readAABB
import ru.dbotthepony.kstarbound.io.readDouble
import ru.dbotthepony.kstarbound.io.readEnumStupid
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeAABB
import ru.dbotthepony.kstarbound.io.writeDouble
import ru.dbotthepony.kstarbound.io.writeEnumStupid
import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.putAll
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.times
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.MovementController
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.Collections
import java.util.stream.Collectors
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.sign

class PlantPieceEntity() : DynamicEntity() {
	override val type: EntityType
		get() = EntityType.PLANT_DROP

	private var calculatedMetaBoundingBox = AABB.ZERO
	private var calculatedCollisionBox = AABB.ZERO
	private var calculatedCollisionHull = Poly.EMPTY

	override val metaBoundingBox: AABB
		get() = calculatedMetaBoundingBox + position

	override val collisionArea: AABB
		get() = calculatedCollisionBox + position

	override val movement = MovementController().also { networkGroup.upstream.add(it.networkGroup) }
	var spawnedDrops by networkedBoolean().also { networkGroup.upstream.add(it) }
		private set

	private val piecesInternal = ArrayList<Piece>()

	val pieces: List<Piece> = Collections.unmodifiableList(piecesInternal)

	var isFirst = false
		private set

	var stemConfig: JsonObject = JsonObject()
		private set
	var foliageConfig: JsonObject = JsonObject()
		private set
	var saplingConfig: JsonElement = JsonNull.INSTANCE
		private set

	var rotationRate = 0.0
		private set
	var rotationFallThreshold = 0.0
		private set
	var rotationCap = 0.0
		private set

	var timeToLive = 30.0
		private set

	@JsonFactory
	data class Piece(
		val image: String,
		val offset: Vector2d,
		val segmentIdx: Int,
		val flip: Boolean,
		val kind: PlantEntity.Piece.Kind,
	) {
		constructor(piece: PlantEntity.Piece) : this(piece.image, piece.offset, piece.segmentIdx, piece.flip, piece.kind)

		constructor(stream: DataInputStream, isLegacy: Boolean) : this(
			stream.readInternedString(),
			stream.readVector2d(isLegacy),
			0, // stream.readIntStupid(isLegacy),
			stream.readBoolean(),
			PlantEntity.Piece.Kind.entries[stream.readEnumStupid(isLegacy)],
		)

		fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeBinaryString(image)
			stream.writeStruct2d(offset, isLegacy)
			// stream.writeIntStupid(segmentIdx, isLegacy)
			stream.writeBoolean(flip)
			stream.writeEnumStupid(kind.ordinal, isLegacy)
		}
	}

	constructor(
		pieces: List<PlantEntity.Piece>,
		position: Vector2d,
		damageSource: Vector2d,
		description: String,
		upsideDown: Boolean,
		stemConfig: JsonObject,
		foliageConfig: JsonObject,
		saplingConfig: JsonElement,
		isFirst: Boolean,
		angle: Double
	) : this() {
		this.stemConfig = stemConfig
		this.foliageConfig = foliageConfig
		this.saplingConfig = saplingConfig
		this.isFirst = isFirst

		this.movement.position = position
		this.description = description

		if (!upsideDown) {
			this.rotationRate = 0.00001 * (damageSource.x + angle).sign
			this.rotationFallThreshold = PI / (3.0 + angle)
			this.rotationCap = PI - this.rotationFallThreshold
		}

		val stemSpaces = pieces.stream().filter { it.isStructuralSegment }.flatMap { it.spaces.stream() }.collect(Collectors.toCollection(::ObjectArraySet))
		val allSpaces = pieces.stream().flatMap { it.spaces.stream() }.collect(Collectors.toCollection(::ObjectArraySet))

		for (piece in pieces) {
			piecesInternal.add(Piece(piece))
		}

		if (allSpaces.isNotEmpty()) {
			calculatedMetaBoundingBox = AABB.ofPoints(allSpaces)

			if (pieces.any { it.isStructuralSegment } && stemSpaces.isNotEmpty()) {
				calculatedCollisionBox = AABB.ofPoints(stemSpaces)

				if (stemSpaces.size >= 2) {
					calculatedCollisionHull = Poly.quickhull(stemSpaces.map { it.toDoubleVector() })
				} else {
					calculatedCollisionHull = Poly(calculatedCollisionBox)
				}
			} else {
				calculatedCollisionBox = calculatedMetaBoundingBox
				calculatedCollisionHull = Poly(calculatedMetaBoundingBox)
			}
		} else {
			calculatedMetaBoundingBox = AABB.rectangle(Vector2d.ZERO, 0.1, 0.1)
			calculatedCollisionBox = calculatedMetaBoundingBox
			calculatedCollisionHull = Poly(calculatedMetaBoundingBox)
		}
	}

	constructor(stream: DataInputStream, isLegacy: Boolean) : this() {
		timeToLive = stream.readDouble(isLegacy)
		isFirst = stream.readBoolean()
		description = stream.readInternedString()
		calculatedMetaBoundingBox = stream.readAABB(isLegacy)
		calculatedCollisionBox = stream.readAABB(isLegacy)
		rotationRate = stream.readDouble(isLegacy)

		piecesInternal.clear()
		piecesInternal.addAll(stream.readCollection { Piece(this, isLegacy) })

		stemConfig = stream.readJsonElement() as JsonObject
		foliageConfig = stream.readJsonElement() as JsonObject
		saplingConfig = stream.readJsonElement()
	}

	constructor(data: JsonObject) : this() {
		deserialize(data)
	}

	override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
		stream.writeDouble(timeToLive, isLegacy)
		stream.writeBoolean(isFirst)
		stream.writeBinaryString(description)
		stream.writeAABB(calculatedMetaBoundingBox, isLegacy)
		stream.writeAABB(calculatedCollisionBox, isLegacy)
		stream.writeDouble(rotationRate, isLegacy)
		stream.writeCollection(piecesInternal) { it.write(this, isLegacy) }
		stream.writeJsonElement(stemConfig)
		stream.writeJsonElement(foliageConfig)
		stream.writeJsonElement(saplingConfig)
	}

	// this is obviously a bad idea, because original engine does not
	// serialize falling plant pieces, but...
	@JsonFactory
	data class JsonData(
		val timeToLive: Double,
		val isFirst: Boolean,
		val description: String,
		val calculatedMetaBoundingBox: AABB,
		val calculatedCollisionBox: AABB,
		val rotationRate: Double,
		val pieces: List<Piece>,
		val stemConfig: JsonObject,
		val foliageConfig: JsonObject,
		val saplingConfig: JsonElement,
	)

	override fun serialize(data: JsonObject) {
		super.serialize(data)

		data.putAll(Starbound.gson.toJsonTree(
			JsonData(timeToLive, isFirst, description, calculatedMetaBoundingBox, calculatedCollisionBox, rotationRate, pieces, stemConfig, foliageConfig, saplingConfig)
		) as JsonObject)
	}

	override fun deserialize(data: JsonObject) {
		super.deserialize(data)

		val (timeToLive, isFirst, description, calculatedMetaBoundingBox, calculatedCollisionBox, rotationRate, pieces, stemConfig, foliageConfig, saplingConfig) = Starbound.gson.fromJson(data, JsonData::class.java)
		this.timeToLive = timeToLive
		this.isFirst = isFirst
		this.description = description
		this.calculatedMetaBoundingBox = calculatedMetaBoundingBox
		this.calculatedCollisionBox = calculatedCollisionBox
		this.rotationRate = rotationRate
		this.piecesInternal.addAll(pieces)
		this.stemConfig = stemConfig
		this.foliageConfig = foliageConfig
		this.saplingConfig = saplingConfig
	}

	override fun onJoinWorld(world: World<*, *>) {
		val parameters = MovementParameters(
			collisionPoly = Either.left(calculatedCollisionHull),
			ignorePlatformCollision = true,
			gravityMultiplier = 0.2,
			physicsEffectCategories = ImmutableSet.of("plantdrop")
		)

		movement.applyParameters(parameters)
		super.onJoinWorld(world)
	}

	override fun tick(delta: Double) {
		super.tick(delta)

		timeToLive -= delta

		if (!isRemote) {
			// TODO: think up a better curve then sin
			val rotationAcceleration = 0.01 * world.gravityAt(position).length * rotationRate.sign * delta

			if (movement.rotation.absoluteValue > rotationCap)
				rotationRate -= rotationAcceleration
			else if (movement.rotation.absoluteValue < rotationFallThreshold)
				rotationRate += rotationAcceleration

			movement.rotation = rotationRate

			if (timeToLive > 0.0) {
				movement.applyParameters(MovementParameters(gravityEnabled = rotationRate.absoluteValue >= rotationFallThreshold))

				if (movement.isOnGround) {
					timeToLive = 0.0
				}
			}

			if ((timeToLive <= 0.0 || world.gravityAt(position).lengthSquared == 0.0) && !spawnedDrops) {
				spawnedDrops = true

				for (piece in piecesInternal) {
					var dropOptions = JsonArray()

					when (piece.kind) {
						PlantEntity.Piece.Kind.NONE -> {}

						PlantEntity.Piece.Kind.STEM -> {
							dropOptions = stemConfig.get("drops", JsonArray())
						}

						PlantEntity.Piece.Kind.FOLIAGE -> {
							dropOptions = foliageConfig.get("drops", JsonArray())
						}
					}

					if (dropOptions.size() > 0) {
						val option = dropOptions.random(world.random).asJsonArray

						for (drop in option) {
							val img = Image.get(piece.image) ?: continue

							var pos = piece.offset + img.size.toDoubleVector() * 0.5 / PIXELS_IN_STARBOUND_UNIT
							pos = pos.rotate(movement.rotation)
							pos += Vector2d(world.random.nextDouble(-0.2, 0.2), world.random.nextDouble(-0.2, 0.2))
							pos += position

							var descriptor = ItemDescriptor(drop)

							if (descriptor.name == "sapling") {
								descriptor = descriptor.copy(parameters = saplingConfig as? JsonObject ?: JsonObject())
							}

							val entity = ItemDropEntity(descriptor)
							entity.position = pos
							entity.joinWorld(world)
						}
					}
				}

				remove(RemovalReason.DYING)
				return
			}
		}

		if (world.isServer && timeToLive <= 0.0) {
			remove(RemovalReason.REMOVED)
		}
	}
}
