package ru.dbotthepony.kstarbound.world.entities

import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.guava.immutableMap
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.DamageType
import ru.dbotthepony.kstarbound.defs.EntityDamageTeam
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.PhysicsForceRegion
import ru.dbotthepony.kstarbound.defs.PhysicsMovingCollision
import ru.dbotthepony.kstarbound.defs.ProjectileDefinition
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.io.readDouble
import ru.dbotthepony.kstarbound.io.writeDouble
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.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.Directives
import ru.dbotthepony.kstarbound.util.asBooleanOrNull
import ru.dbotthepony.kstarbound.util.asDoubleOrNull
import ru.dbotthepony.kstarbound.util.asStringOrNull
import ru.dbotthepony.kstarbound.util.valueOf
import java.io.DataInputStream
import java.io.DataOutputStream

class ProjectileEntity private constructor(val config: Registry.Entry<ProjectileDefinition>, private val parameters: JsonObject) : DynamicEntity() {
	override val type: EntityType
		get() = EntityType.PROJECTILE

	// TODO: make it persistent
	override val isPersistent: Boolean
		get() = false

	constructor(config: Registry.Entry<ProjectileDefinition>, data: DataInputStream, isLegacy: Boolean) : this(config, data.readJsonElement() as JsonObject) {
		sourceEntityId = if (isLegacy) data.readSignedVarInt() else data.readInt()
		trackSourceEntity = data.readBoolean()

		initialSpeed = data.readDouble(isLegacy)
		powerMultiplier = data.readDouble(isLegacy)

		team.accept(EntityDamageTeam(data, isLegacy))
	}

	override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
		stream.writeBinaryString(config.key)
		stream.writeJsonElement(parameters)
		if (isLegacy) stream.writeSignedVarInt(sourceEntityId) else stream.writeInt(sourceEntityId)
		stream.writeBoolean(trackSourceEntity)
		stream.writeDouble(initialSpeed, isLegacy)
		stream.writeDouble(powerMultiplier, isLegacy)
		team.get().write(stream, isLegacy)
	}

	override val metaBoundingBox: AABB
		get() = config.value.boundBox + position

	override val movement: MovementController = MovementController()

	init {
		if ("uniqueId" in parameters) {
			this.uniqueID.accept(parameters["uniqueId"].asString)
		}
	}

	var acceleration = parameters.get("acceleration", config.value.acceleration)
	var power = parameters.get("power", config.value.power)
	var powerMultiplier = parameters.get("powerMultiplier", config.value.power)
	var imageDirectives = Directives(parameters.get("processing", ""))
	var persistentAudio = AssetPathStack.relativeTo(config.file?.computeDirectory(true) ?: "", parameters.get("persistentAudio", config.value.persistentAudio.fullPath))
	var damageKind = parameters.get("damageKind", config.value.damageKind)
	var damageType = DamageType.entries.valueOf(parameters.get("damageType", config.value.damageType))
	var rayCheckToSource = parameters.get("rayCheckToSource", config.value.rayCheckToSource)
	var damageRepeatGroup = parameters["damageRepeatGroup"]?.asStringOrNull ?: config.value.damageRepeatGroup
	var damageRepeatTimeout = parameters["damageRepeatTimeout"]?.asDoubleOrNull ?: config.value.damageRepeatTimeout
	var falldown = parameters["falldown"]?.asBooleanOrNull ?: config.value.falldown
	var hydrophobic = parameters["hydrophobic"]?.asBooleanOrNull ?: config.value.hydrophobic
	var onlyHitTerrain = parameters["onlyHitTerrain"]?.asBooleanOrNull ?: config.value.onlyHitTerrain

	init {
		if ("damageTeam" in parameters) {
			team.accept(Starbound.gson.fromJson(parameters["damageTeam"])!!)
		} else if (config.value.damageTeam != null) {
			team.accept(config.value.damageTeam!!)
		}

		var movementParams = config.value.actualMovementSettings.get()

		if ("movementSettings" in parameters) {
			movementParams = movementParams.merge(Starbound.gson.fromJson(parameters["movementSettings"])!!)
		}

		if (movementParams.physicsEffectCategories == null) {
			movementParams = movementParams.copy(physicsEffectCategories = ImmutableSet.of("projectile"))
		}

		movement.applyParameters(movementParams)
	}

	var initialSpeed = parameters["speed"]?.asDoubleOrNull ?: config.value.speed

	private var lastSourceEntityPosition: Vector2d? = null
	var sourceEntityId: Int = 0

	var sourceEntity: AbstractEntity?
		get() = if (isInWorld) world.entities[sourceEntityId] else null
		set(value) {
			sourceEntityId = value?.entityID ?: 0
		}

	var trackSourceEntity = false
	var bounces = parameters["bounces"]?.asInt ?: config.value.bounces
	var frame = 0
	var animationTimer = 0.0
	var animationCycle = parameters["animationCycle"]?.asDouble ?: config.value.animationCycle
	var collision = false

	inner class ForceRegion(val region: PhysicsForceRegion) {
		var enabled by networkedBoolean(region.enabled).also { networkGroup.upstream.add(it) }
	}

	inner class MovingCollision(val collision: PhysicsMovingCollision) {
		var enabled by networkedBoolean(collision.enabled).also { networkGroup.upstream.add(it) }
	}

	val physicsForces = immutableMap<String, ForceRegion> {
		for (k in config.value.physicsForces.keys.sorted()) {
			put(k, ForceRegion(config.value.physicsForces[k]!!))
		}
	}

	val physicsCollisions = immutableMap<String, MovingCollision> {
		for (k in config.value.physicsCollisions.keys.sorted()) {
			put(k, MovingCollision(config.value.physicsCollisions[k]!!))
		}
	}

	val collisionEvent = networkedEventCounter().also { networkGroup.upstream.add(it) }

	init {
		networkGroup.upstream.add(movement.networkGroup)
	}

	val emitter = EffectEmitter(this).also { networkGroup.upstream.add(it.networkGroup) }

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