package ru.dbotthepony.kstarbound.world.entities

import com.google.common.collect.ImmutableSet
import com.google.gson.JsonElement
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.network.syncher.networkedEnum
import ru.dbotthepony.kstarbound.network.syncher.networkedItem
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.util.GameTimer
import ru.dbotthepony.kstarbound.util.RelativeClock
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.Predicate
import kotlin.math.min

class ItemDropEntity() : DynamicEntity() {
	// int32_t, but networked as proper enum
	// костыль (именно DEAD состояние), но требуют оригинальные клиенты
	// мда.
	enum class State(override val jsonName: String) : IStringSerializable {
		INTANGIBLE("Intangible"),
		AVAILABLE("Available"),
		TAKEN("Taken"),
		DEAD("Dead");
	}

	var state by networkedEnum(State.entries).also { networkGroup.upstream.add(it) }
		private set
	var owningEntity by networkedSignedInt().also { networkGroup.upstream.add(it) }
		private set
	override val movement: MovementController = MovementController().also { networkGroup.upstream.add(it.networkGroup) }
	var item by networkedItem().also { networkGroup.upstream.add(it) }
		private set

	var shouldNotExpire = false
	val age = RelativeClock()
	var intangibleTimer = GameTimer(0.0)
		private set

	init {
		movement.applyParameters(Globals.itemDrop.movementSettings)

		if (movement.movementParameters.physicsEffectCategories == null) {
			movement.applyParameters(MovementParameters(physicsEffectCategories = itemdropCat))
		}

		movement.applyParameters(MovementParameters(collisionPoly = Either.left(Poly(AABB(Vector2d(-0.5, -0.5), Vector2d(0.5, 0.5))))))
	}

	constructor(item: ItemDescriptor) : this() {
		this.item = item.build()
		this.owningEntity = 0
		this.state = State.AVAILABLE
	}

	constructor(item: ItemStack) : this() {
		this.item = item.copy()
		this.owningEntity = 0
		this.state = State.AVAILABLE
	}

	constructor(stream: DataInputStream, isLegacy: Boolean) : this() {
		item = ItemDescriptor(stream).build()
		shouldNotExpire = stream.readBoolean()
		age.read(stream, isLegacy)
		intangibleTimer = GameTimer(stream, isLegacy)
	}

	override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
		item.write(stream)
		stream.writeBoolean(shouldNotExpire)
		age.write(stream, isLegacy)
		intangibleTimer.write(stream, isLegacy)
	}

	fun setIntangibleTime(time: Double) {
		intangibleTimer = GameTimer(time)

		if (state == State.AVAILABLE)
			state = State.INTANGIBLE
	}

	override val type: EntityType
		get() = EntityType.ITEM_DROP

	val canTake: Boolean get() {
		return state == State.AVAILABLE && owningEntity == 0 && item.isNotEmpty
	}

	fun take(by: AbstractEntity): ItemStack {
		if (canTake) {
			state = State.TAKEN
			age.set(0.0)
			owningEntity = by.entityID
			return item.copy()
		}

		return ItemStack.EMPTY
	}

	private var stayAliveFor = -1.0

	override val metaBoundingBox: AABB
		get() = AABB(position - Vector2d(0.5, 0.5), position + Vector2d(0.5, 0.5))

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

		if (!isRemote) {
			if (item.isEmpty) {
				// remove from world
				if (isInWorld) // got removed by other item
					remove(RemovalReason.REMOVED)
				return
			}

			if (state != State.TAKEN)
				age.update(world.sky.time)
			else if (stayAliveFor > 0.0) {
				stayAliveFor -= delta

				if (stayAliveFor <= 0.0) {
					state = State.DEAD
					remove(RemovalReason.REMOVED)
					return
				}
			}

			if (owningEntity != 0) {
				// move towards picking player
				val entity = world.entities[owningEntity]

				if (entity == null) {
					// Our owning entity left, disappear quickly
					state = State.DEAD
					remove(RemovalReason.REMOVED)
				} else if (stayAliveFor == -1.0) {
					val diff = world.geometry.diff(entity.position, position)
					movement.approachVelocity(diff.unitVector * Globals.itemDrop.velocity, Globals.itemDrop.velocityApproach, delta)

					if (diff.length < Globals.itemDrop.pickupDistance) {
						stayAliveFor = Starbound.TIMESTEP * 4.0 // stay alive a little longer so pickup "animation" doesn't get cut off early
					}
				}

				movement.applyParameters(noGravity)
			} else {
				// Rarely, check for other drops near us and combine with them if possible.
				if (canTake && world.random.nextFloat() < Globals.itemDrop.combineChance && item.size < item.maxStackSize) {
					val find = world.entityIndex.query(Globals.itemDrop.combineRadiusBox + position, filter = Predicate {
						it is ItemDropEntity && it !== this && it.canTake && it.item.size != it.item.maxStackSize && it.position.distance(position) <= Globals.itemDrop.combineRadius && it.item.isStackable(item) })

					for (entity in find) {
						entity as ItemDropEntity
						val newSize = min(item.size + entity.item.size, item.maxStackSize)
						val diff = newSize - item.size
						if (diff <= 0) break
						item.size += diff

						if (entity.item.size == diff) {
							// we need to do this instead of updating item stack size
							// because if we network empty itemstack legacy clients will crash
							// because of no safeguard check inside ItemDrop::render
							// Clients will crash anyway if we network item they don't know about, lol.
							entity.state = State.DEAD
							entity.remove(RemovalReason.REMOVED)
						} else {
							entity.item.size -= diff
						}

						entity.item.size -= diff
						age.set(min(age.time, entity.age.time))

						// Average the position and velocity of the drop we merged with
						//movement.position += world.geometry.diff(movement.position, entity.movement.position) / 2.0
						//movement.velocity += world.geometry.diff(movement.velocity, entity.movement.velocity) / 2.0
					}
				}

				movement.applyParameters(gravity)
			}

			intangibleTimer.tick(delta)

			if (state == State.INTANGIBLE && intangibleTimer.hasFinished)
				state = State.AVAILABLE

			//if (!shouldNotExpire && state == State.AVAILABLE && age.time > )
		}
	}

	companion object {
		private val itemdropCat = ImmutableSet.of("itemdrop")
		private val noGravity = MovementParameters(collisionEnabled = false, gravityEnabled = false)
		private val gravity = MovementParameters(collisionEnabled = true, gravityEnabled = true)
	}
}
