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

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
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.JsonPrimitive
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.util.Either
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.JsonReference
import ru.dbotthepony.kstarbound.defs.actor.StatModifier
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.json.listAdapter
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.math.PeriodicFunction
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World

@JsonAdapter(ObjectDefinition.Adapter::class)
data class ObjectDefinition(
	val objectName: String,
	val objectType: ObjectType = ObjectType.OBJECT,
	val race: String = "generic",
	val category: String = "other",
	val colonyTags: ImmutableSet<String> = ImmutableSet.of(),
	val scripts: ImmutableSet<AssetPath> = ImmutableSet.of(),
	val animationScripts: ImmutableSet<AssetPath> = ImmutableSet.of(),
	val hasObjectItem: Boolean = true,
	val scannable: Boolean = true,
	val retainObjectParametersInItem: Boolean = false,
	val breakDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
	// null - not specified, empty list - always drop nothing
	val breakDropOptions: ImmutableList<ImmutableList<ItemDescriptor>>? = null,
	val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
	val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(),
	val animation: AssetReference<AnimationDefinition>? = null,
	val animationCustom: JsonObject = JsonObject(),
	val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(),
	val smashParticles: JsonArray? = null,
	val smashable: Boolean = false,
	val unbreakable: Boolean = false,
	val damageShakeMagnitude: Double = 0.2,
	val damageMaterialKind: String = "solid",
	val damageTeam: DamageTeam = DamageTeam(),
	val lightColor: RGBAColor? = null,
	val lightColors: ImmutableMap<String, RGBAColor> = ImmutableMap.of(),
	val pointLight: Boolean = false,
	val pointBeam: Double = 0.0,
	val beamAmbience: Double = 0.0,
	val soundEffect: AssetPath? = null,
	val soundEffectRangeMultiplier: Double = 1.0,
	val price: Long = 1L,
	val statusEffects: ImmutableList<Either<String, StatModifier>> = ImmutableList.of(),
	val touchDamage: JsonReference.Object = JsonReference.Object(null, null, null),
	val minimumLiquidLevel: Float? = null,
	val maximumLiquidLevel: Float? = null,
	val liquidCheckInterval: Float = 0.5f,
	val health: Double = 1.0,
	val rooting: Boolean = false,
	val biomePlaced: Boolean = false,
	val printable: Boolean = false,
	val smashOnBreak: Boolean = false,
	val damageConfig: TileDamageConfig,
	val flickerPeriod: PeriodicFunction? = null,
	val orientations: ImmutableList<ObjectOrientation>,
) {
	fun findValidOrientation(world: World<*, *>, position: Vector2i, directionAffinity: Direction? = null, ignoreProtectedDungeons: Boolean = false): Int {
		// If we are given a direction affinity, try and find an orientation with a
		// matching affinity *first*
		if (directionAffinity != null) {
			for ((i, orientation) in orientations.withIndex()) {
				if (orientation.directionAffinity == directionAffinity && orientation.placementValid(world, position, ignoreProtectedDungeons) && orientation.anchorsValid(world, position))
					return i
			}
		}

		// Then, fallback and try and find any valid affinity
		for ((i, orientation) in orientations.withIndex()) {
			if (orientation.placementValid(world, position) && orientation.anchorsValid(world, position))
				return i
		}

		return -1
	}

	class Adapter(gson: Gson) : TypeAdapter<ObjectDefinition>() {
		@JsonFactory(logMisses = false)
		data class PlainData(
			val objectName: String,
			val objectType: ObjectType = ObjectType.OBJECT,
			val race: String = "generic",
			val category: String = "other",
			val colonyTags: ImmutableSet<String> = ImmutableSet.of(),
			val scripts: ImmutableSet<AssetPath> = ImmutableSet.of(),
			val animationScripts: ImmutableSet<AssetPath> = ImmutableSet.of(),
			val hasObjectItem: Boolean = true,
			val scannable: Boolean = true,
			val retainObjectParametersInItem: Boolean = false,
			val breakDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
			// null - not specified, empty list - always drop nothing
			val breakDropOptions: ImmutableList<ImmutableList<ItemDescriptor>>? = null,
			val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
			val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(),
			val animation: AssetReference<AnimationDefinition>? = null,
			val animationCustom: JsonObject = JsonObject(),
			//val animation: AssetPath? = null,
			val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(),
			val smashParticles: JsonArray? = null,
			val smashable: Boolean = false,
			val unbreakable: Boolean = false,
			val damageShakeMagnitude: Double = 0.2,
			val damageMaterialKind: String = "solid",
			val damageTeam: DamageTeam = DamageTeam(),
			val lightColor: RGBAColor? = null,
			val lightColors: ImmutableMap<String, RGBAColor> = ImmutableMap.of(),
			val pointLight: Boolean = false,
			val pointBeam: Double = 0.0,
			val beamAmbience: Double = 0.0,
			val soundEffect: AssetPath? = null,
			val soundEffectRangeMultiplier: Double = 1.0,
			val price: Long = 1L,
			val statusEffects: ImmutableList<Either<String, StatModifier>> = ImmutableList.of(),
			//val touchDamage: TouchDamage,
			val touchDamage: JsonReference.Object = JsonReference.Object(null, null, null),
			val minimumLiquidLevel: Float? = null,
			val maximumLiquidLevel: Float? = null,
			val liquidCheckInterval: Float = 0.5f,
			val health: Double = 1.0,
			val rooting: Boolean = false,
			val biomePlaced: Boolean = false,
		)

		private val objectRef = gson.getAdapter(JsonReference.Object::class.java)
		private val basic = gson.getAdapter(PlainData::class.java)
		private val damageConfig = gson.getAdapter(TileDamageConfig::class.java)
		private val damageTeam = gson.getAdapter(DamageTeam::class.java)
		private val orientations = gson.getAdapter(ObjectOrientation::class.java)
		private val emitter = gson.getAdapter(ParticleEmissionEntry::class.java)
		private val emitters = gson.listAdapter<ParticleEmissionEntry>()

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

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

			val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
			val basic = basic.fromJsonTree(read)

			val printable = basic.hasObjectItem && read.get("printable", basic.scannable)
			val smashOnBreak = read.get("smashOnBreak", basic.smashable)

			val getDamageParams = objectRef.fromJsonTree(read.get("damageTable", JsonPrimitive("/objects/defaultParameters.config:damageTable")))
			getDamageParams?.value ?: throw JsonSyntaxException("No valid damageTable specified")

			getDamageParams.value["health"] = read["health"]
			getDamageParams.value["harvestLevel"] = read["harvestLevel"]

			val damageConfig = damageConfig.fromJsonTree(getDamageParams.value)

			val flickerPeriod = if ("flickerPeriod" in read) {
				PeriodicFunction(
					read.get("flickerPeriod", 0.0),
					read.get("flickerMinIntensity", 0.0),
					read.get("flickerMaxIntensity", 0.0),
					read.get("flickerPeriodVariance", 0.0),
					read.get("flickerIntensityVariance", 0.0),
				)
			} else {
				null
			}

			val orientations = ObjectOrientation.preprocess(read.getArray("orientations"))
				.stream()
				.map { orientations.fromJsonTree(it) }
				.collect(ImmutableList.toImmutableList())

			if ("particleEmitter" in read) {
				orientations.forEach { it.particleEmitters.add(emitter.fromJsonTree(read["particleEmitter"])) }
			}

			if ("particleEmitters" in read) {
				orientations.forEach { it.particleEmitters.addAll(emitters.fromJsonTree(read["particleEmitters"])) }
			}

			return ObjectDefinition(
				objectName = basic.objectName,
				objectType = basic.objectType,
				race = basic.race,
				category = basic.category,
				colonyTags = basic.colonyTags,
				scripts = basic.scripts,
				animationScripts = basic.animationScripts,
				hasObjectItem = basic.hasObjectItem,
				scannable = basic.scannable,
				retainObjectParametersInItem = basic.retainObjectParametersInItem,
				breakDropPool = basic.breakDropPool,
				breakDropOptions = basic.breakDropOptions,
				smashDropPool = basic.smashDropPool,
				smashDropOptions = basic.smashDropOptions,
				animation = basic.animation,
				animationCustom = basic.animationCustom,
				smashSounds = basic.smashSounds,
				smashParticles = basic.smashParticles,
				smashable = basic.smashable,
				unbreakable = basic.unbreakable,
				damageShakeMagnitude = basic.damageShakeMagnitude,
				damageMaterialKind = basic.damageMaterialKind,
				damageTeam = basic.damageTeam,
				lightColor = basic.lightColor,
				lightColors = basic.lightColors,
				pointLight = basic.pointLight,
				pointBeam = basic.pointBeam,
				beamAmbience = basic.beamAmbience,
				soundEffect = basic.soundEffect,
				soundEffectRangeMultiplier = basic.soundEffectRangeMultiplier,
				price = basic.price,
				statusEffects = basic.statusEffects,
				touchDamage = basic.touchDamage,
				minimumLiquidLevel = basic.minimumLiquidLevel,
				maximumLiquidLevel = basic.maximumLiquidLevel,
				liquidCheckInterval = basic.liquidCheckInterval,
				health = basic.health,
				rooting = basic.rooting,
				biomePlaced = basic.biomePlaced,
				printable = printable,
				smashOnBreak = smashOnBreak,
				damageConfig = damageConfig,
				flickerPeriod = flickerPeriod,
				orientations = orientations,
			)
		}
	}
}
