package ru.dbotthepony.kstarbound.defs

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeDouble
import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.io.readDouble
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readMVariant2
import ru.dbotthepony.kstarbound.io.readNullableDouble
import ru.dbotthepony.kstarbound.io.readNullableString
import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeDouble
import ru.dbotthepony.kstarbound.io.writeMVariant2
import ru.dbotthepony.kstarbound.io.writeNullable
import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.getAdapter
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream

// uint8_t
enum class TeamType(override val jsonName: String) : IStringSerializable {
	NULL("null"),
	// non-PvP-enabled players and player allied NPCs
	FRIENDLY("friendly"),
	// hostile and neutral NPCs and monsters
	ENEMY("enemy"),
	// PvP-enabled players
	PVP("pvp"),
	// cannot damage anything, can be damaged by Friendly/PVP/Assistant
	PASSIVE("passive"),
	// cannot damage or be damaged
	GHOSTLY("ghostly"),
	// cannot damage enemies, can be damaged by anything except enemy
	ENVIRONMENT("environment"),
	// damages anything except ghostly, damaged by anything except ghostly/passive
	// used for self damage
	INDISCRIMINATE("indiscriminate"),
	// cannot damage friendlies and cannot be damaged by anything
	ASSISTANT("assistant");
}

// int32_t
enum class HitType(override val jsonName: String) : IStringSerializable {
	HIT("Hit"),
	STRONG_HIT("StrongHit"),
	WEAK_HIT("WeakHit"),
	SHIELD_HIT("ShieldHit"),
	KILL("Kill");
}

// uint8_t
enum class DamageType(override val jsonName: String) : IStringSerializable {
	NO_DAMAGE("NoDamage"),
	DAMAGE("Damage"),
	IGNORE_DEFENCE("IgnoresDef"),
	KNOCKBACK("IgnoresDef"),
	ENVIRONMENT("Environment"),
	STATUS("Environment");
}

@JsonFactory
data class EntityDamageTeam(val type: TeamType = TeamType.NULL, val team: Int = 0) {
	constructor(stream: DataInputStream, isLegacy: Boolean) : this(TeamType.entries[stream.readUnsignedByte()], stream.readUnsignedShort())

	fun write(stream: DataOutputStream, isLegacy: Boolean) {
		stream.writeByte(type.ordinal)
		stream.writeShort(team)
	}

	companion object {
		val NULL = EntityDamageTeam()
		val PASSIVE = EntityDamageTeam(TeamType.PASSIVE)
		val CODEC = nativeCodec(::EntityDamageTeam, EntityDamageTeam::write)
		val LEGACY_CODEC = legacyCodec(::EntityDamageTeam, EntityDamageTeam::write)
	}
}

@JsonFactory
data class TouchDamage(
	val poly: ImmutableList<Vector2d> = ImmutableList.of(),
	val teamType: TeamType = TeamType.ENVIRONMENT,
	val damage: Double = 0.0,
	val damageSourceKind: String = "",
	val knockback: Double = 0.0,
	val statusEffects: ImmutableSet<String> = ImmutableSet.of(),
)

// this shit is a complete mess, because in original code DamageSource::toJson() method
// will create json structure which will not be readable by DamageSource's constructor
// (will always throw an exception)
@JsonAdapter(DamageSource.Adapter::class)
data class DamageSource(
	val damageType: DamageType,
	val damageArea: Either<Poly, Line2d>,
	val damage: Double,
	val trackSourceEntity: Boolean,
	val sourceEntityId: Int = 0,
	val team: EntityDamageTeam = EntityDamageTeam.PASSIVE,
	val damageRepeatGroup: String? = null,
	val damageRepeatTimeout: Double? = null,
	val damageSourceKind: String = "",
	val statusEffects: ImmutableList<EphemeralStatusEffect> = ImmutableList.of(),
	val knockback: Either<Double, Vector2d>,
	val rayCheck: Boolean = false,
) {
	constructor(stream: DataInputStream, isLegacy: Boolean) : this(
		DamageType.entries[stream.readUnsignedByte()],
		stream.readMVariant2({ Poly.read(this, isLegacy) }, { Line2d(stream, isLegacy) }) ?: throw IllegalArgumentException("Empty MVariant damageArea"),
		stream.readDouble(isLegacy),
		stream.readBoolean(),
		stream.readInt(),
		EntityDamageTeam(stream, isLegacy),
		stream.readNullableString(),
		stream.readNullableDouble(isLegacy),
		stream.readInternedString(),
		ImmutableList.copyOf(stream.readCollection { EphemeralStatusEffect(stream, isLegacy) }),
		stream.readMVariant2({ readDouble(isLegacy) }, { readVector2d(isLegacy) }) ?: throw IllegalArgumentException("Empty MVariant knockback"),
		stream.readBoolean()
	)

	data class JsonData(
		val poly: Poly? = null,
		val line: Line2d? = null,
		val damage: Double,
		val damageType: DamageType = DamageType.DAMAGE,
		val trackSourceEntity: Boolean = true,
		val sourceEntityId: Int = 0,
		val teamType: TeamType = TeamType.PASSIVE,
		val teamNumber: Int = 0,
		val team: EntityDamageTeam? = null,
		val damageRepeatGroup: String? = null,
		val damageRepeatTimeout: Double? = null,
		val damageSourceKind: String = "",
		val statusEffects: ImmutableList<EphemeralStatusEffect> = ImmutableList.of(),
		val knockback: Either<Double, Vector2d> = Either.left(0.0),
		val rayCheck: Boolean = false,
	)

	fun write(stream: DataOutputStream, isLegacy: Boolean) {
		stream.writeByte(damageType.ordinal)
		stream.writeMVariant2(damageArea, { it.write(stream, isLegacy) }, { it.write(stream, isLegacy) })
		stream.writeDouble(damage, isLegacy)
		stream.writeBoolean(trackSourceEntity)
		stream.writeInt(sourceEntityId)
		team.write(stream, isLegacy)
		stream.writeNullable(damageRepeatGroup, DataOutputStream::writeBinaryString)
		stream.writeNullable(damageRepeatTimeout) { writeDouble(it, isLegacy) }
		stream.writeBinaryString(damageSourceKind)
		stream.writeCollection(statusEffects) { it.write(stream, isLegacy) }
		stream.writeMVariant2(knockback, { writeDouble(it, isLegacy) }, { writeStruct2d(it, isLegacy) })
		stream.writeBoolean(rayCheck)
	}

	class Adapter(gson: Gson) : TypeAdapter<DamageSource>() {
		private val data = FactoryAdapter.createFor(JsonData::class, gson = gson)

		override fun write(out: JsonWriter, value: DamageSource) {
			data.write(out, JsonData(
				poly = value.damageArea.left.orNull(),
				line = value.damageArea.right.orNull(),
				damage = value.damage,
				damageType = value.damageType,
				trackSourceEntity = value.trackSourceEntity,
				sourceEntityId = value.sourceEntityId,
				team = value.team,
				damageRepeatGroup = value.damageRepeatGroup,
				damageRepeatTimeout = value.damageRepeatTimeout,
				damageSourceKind = value.damageSourceKind,
				statusEffects = value.statusEffects,
				knockback = value.knockback,
				rayCheck = value.rayCheck,
			))
		}

		override fun read(`in`: JsonReader): DamageSource {
			val read = data.read(`in`)

			return DamageSource(
				damageType = read.damageType,
				damageArea = if (read.line == null) Either.left(read.poly ?: throw JsonSyntaxException("Missing both 'line' and 'poly' from DamageSource json")) else Either.right(read.line),
				damage = read.damage,
				trackSourceEntity = read.trackSourceEntity,
				sourceEntityId = read.sourceEntityId,
				statusEffects = read.statusEffects,
				team = read.team ?: EntityDamageTeam(read.teamType, read.teamNumber),
				damageRepeatGroup = read.damageRepeatGroup,
				damageRepeatTimeout = read.damageRepeatTimeout,
				damageSourceKind = read.damageSourceKind,
				knockback = read.knockback,
				rayCheck = read.rayCheck,
			)
		}
	}

	companion object {
		val CODEC = nativeCodec(::DamageSource, DamageSource::write)
		val LEGACY_CODEC = legacyCodec(::DamageSource, DamageSource::write)
	}
}
