package ru.dbotthepony.kstarbound.world.entities

import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.io.IntValueCodec
import ru.dbotthepony.kommons.io.map
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.matrix.Matrix3d
import ru.dbotthepony.kommons.matrix.Matrix3f
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.animation.AnimatedPartsDefinition
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.animation.ParticleFactory
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.math.PeriodicFunction
import ru.dbotthepony.kstarbound.math.approachAngle
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.NetworkedList
import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap
import ru.dbotthepony.kstarbound.network.syncher.NetworkedSignal
import ru.dbotthepony.kstarbound.network.syncher.networkedAABBNullable
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedColor
import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter
import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedPointer
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.network.syncher.networkedString
import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.*
import java.util.function.Consumer
import kotlin.NoSuchElementException
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt

// NetworkedAnimator + AnimatedPartSet combined into one class

// AnimatedPartSet original docs:
// Defines a "animated" data set constructed in such a way that it is very
// useful for doing generic animations with lots of additional animation data.
// It is made up of two concepts, "states" and "parts".
//
// States:
//
// There are N "state types" defined, which each defines a set of mutually
// exclusive states that each "state type" can be in.  For example, one state
// type might be "movement", and the "movement" states might be "idle", "walk",
// and "run.  Another state type might be "attack" which could have as its
// states "idle", and "melee".  Each state type will have exactly one currently
// active state, so this class may, for example,  be in the total state of
// "movement:idle" and "attack:melee".  Each state within each state type is
// animated, so that over time the state frame increases and may loop around,
// or transition into another state so that that state type without interaction
// may go from "melee" to "idle" when the "melee" state animation is finished.
// This is defined by the individual state config in the configuration passed
// into the constructor.
//
// Parts:
//
// Each instance of this class also can have N "Parts" defined, which are
// groups of properties that "listen" to active states.  Each part can "listen"
// to one or more state types, and the first matching state x state type pair
// (in order of state type priority which is specified in the config) is
// chosen, and the properties from that state type and state are merged into
// the part to produce the final active part information.  Rather than having a
// single image or image set for each part, since this class is intended to be
// as generic as possible, all of this data is assumed to be queried from the
// part properties, so that things such as image data as well as other things
// like damage or collision polys can be stored along with the animation
// frames, the part state, the base part, whichever is most applicable.
class Animator() {
	private class Light {
		private val elements = ArrayList<NetworkedElement>()

		fun addTo(group: NetworkedGroup) {
			elements.forEach { group.add(it) }
		}

		var active by networkedBoolean().also { elements.add(it) }
		var xPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear }
		var yPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear }
		var color by networkedColor().also { elements.add(it) }
		var pointAngle by networkedFixedPoint(0.01).also { elements.add(it); it.interpolator = Interpolator.Linear }

		var anchorPart: String? = null
		var transformationGroups: List<String> = listOf()
		var rotationGroup: String? = null
		var rotationCenter: Vector2d? = null

		var flicker: PeriodicFunction? = null
		var pointLight: Boolean = false
		var pointBeam: Float = 0f
		var beamAmbience: Float = 0f
	}

	private enum class SoundSignal {
		PLAY, STOP_ALL;

		companion object {
			val CODEC = IntValueCodec.map({ entries[this] }, { ordinal })
		}
	}

	private class Sound {
		private val elements = ArrayList<NetworkedElement>()

		fun addTo(group: NetworkedGroup) {
			elements.forEach { group.add(it) }
		}

		var rangeMultiplier = 0.0

		val soundPool = NetworkedList(InternedStringCodec).also { elements.add(it) }
		var xPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear }
		var yPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear }
		var volumeTarget by networkedFloat(1.0).also { elements.add(it) }
		var volumeRampTime by networkedFloat(0.0).also { elements.add(it) }
		var pitchMultiplierTarget by networkedFloat(1.0).also { elements.add(it) }
		var pitchMultiplierRampTime by networkedFloat(0.0).also { elements.add(it) }
		var loops by networkedSignedInt().also { elements.add(it) }
		val signals = NetworkedSignal(SoundSignal.CODEC).also { elements.add(it) }
	}

	private class Effect(val type: String, val time: Double, val directives: String) {
		val enabled = networkedBoolean()
		var timer: Double = 0.0

		fun tick(delta: Double) {
			if (timer <= 0.0)
				timer = time
			else
				timer -= delta
		}
	}

	private class StateType(config: AnimatedPartsDefinition.StateType) {
		// NetworkedAnimator
		private var noPropagate = false
		val stateIndex = networkedPointer(-1L)
		val startedEvent = networkedEventCounter()

		// AnimatedPartSet
		var enabled = config.enabled
		var activeStateDirty = true
		val priority = config.priority
		val stateTypeProperties = config.properties
		val default: String
		var activeProperties = JsonObject()
			private set

		// sorted by key
		// Basically, this is definition of each separate state
		val states = config.sortedStates

		var timer = 0.0
		var frame = 0
			private set(value) {
				if (field != value) {
					field = value
					frameChanged = true
				}
			}

		init {
			if (states.isNotEmpty() && config.default.isBlank())
				default = states.keys.first()
			else
				default = config.default
		}

		private var activeStateChanged = false
		private var frameChanged = false

		var activeState: AnimatedPartsDefinition.StateType.State? = null
			set(value) {
				activeStateChanged = true

				if (value == null) {
					if (!noPropagate) {
						try {
							noPropagate = true
							stateIndex.accept(-1L)
						} finally {
							noPropagate = false
						}
					}

					field = null
				} else {
					if (!noPropagate) {
						try {
							noPropagate = true
							stateIndex.accept(value.index.toLong())
						} finally {
							noPropagate = false
						}
					}

					field = value
				}
			}

		fun set(state: String, alwaysStart: Boolean): Boolean {
			val getState = states[state] ?: return false

			if (activeState?.name != getState.name || alwaysStart) {
				noPropagate = true

				try {
					activeState = getState
					stateIndex.accept(getState.index.toLong())
					timer = 0.0
					startedEvent.trigger()
				} finally {
					noPropagate = false
				}

				return true
			}

			return false
		}

		init {
			if (default in states) {
				noPropagate = true
				activeState = states[default]!!
				stateIndex.accept(activeState!!.index.toLong())
				startedEvent.trigger()
				noPropagate = false
			}

			stateIndex.addListener(Consumer {
				if (noPropagate) return@Consumer

				if (it == -1L) {
					try {
						noPropagate = true
						activeState = null
					} finally {
						noPropagate = false
					}
				} else {
					try {
						noPropagate = true
						val newState = states[states.keys.elementAtOrNull(it.toInt()) ?: throw IllegalArgumentException("Unknown animation state $it!")]!!
						activeState = newState
						timer = 0.0
						// don't call startedEvent since this change originates from remote, and it sets "startedEvent" on its own
						// startedEvent.trigger()
					} finally {
						noPropagate = false
					}
				}
			})
		}

		fun tick(delta: Double) {
			var activeState = activeState ?: return
			timer += delta

			if (timer > activeState.cycle) {
				when (activeState.mode) {
					AnimatedPartsDefinition.AnimationMode.END -> timer = activeState.cycle

					AnimatedPartsDefinition.AnimationMode.TRANSITION -> {
						activeState = states[activeState.transition]!! // validity of 'transition' is checked during json load
						this.activeState = activeState
						timer = 0.0
					}

					AnimatedPartsDefinition.AnimationMode.LOOP -> timer %= activeState.cycle
				}
			}

			frame = (timer / activeState.cycle * activeState.frames).toInt().coerceIn(0, activeState.frames - 1)

			if (activeStateChanged || frameChanged) {
				activeProperties = mergeJson(stateTypeProperties.deepCopy(), activeState.properties)
				frameChanged = false
				activeStateChanged = false

				for ((key, values) in activeState.frameProperties) {
					if (values.size() >= frame) {
						activeProperties[key] = values[frame].deepCopy()
					}
				}
			}
		}

		fun finishAnimations() {
			while (true) {
				val active = activeState ?: break

				if (active.mode == AnimatedPartsDefinition.AnimationMode.END) {
					timer = active.cycle
				} else if (active.mode == AnimatedPartsDefinition.AnimationMode.TRANSITION) {
					activeState = states[active.transition]!! // validity of 'transition' is checked during json load
					this.activeState = activeState
					timer = 0.0
					continue
				}

				break
			}
		}
	}

	private inner class Part(private val config: AnimatedPartsDefinition.Part) {
		var activePartDirty = true
		var properties: JsonObject = config.properties.deepCopy()
			private set

		var activeState: AnimatedPartsDefinition.Part.State? = null
			private set

		fun freshen() {
			if (!activePartDirty)
				return

			activePartDirty = false

			// First reset all the active part information assuming that no state type
			// x state match exists.
			activeState = null
			properties = config.properties.deepCopy()

			// Then go through each of the state types and states and look for a part
			// state match in order of priority.

			// honestly, i have no idea what next code does
			for ((stateTypeName, stateType) in stateTypes) {
				// Skip disabled state types
				if (!stateType.enabled)
					continue

				val partStateType = config.partStates[stateTypeName] ?: continue
				val stateName = stateType.activeState?.name ?: continue
				val partState = partStateType[stateName] ?: continue

				activeState = partState
				val frame = stateType.frame

				// Then set the part state data, as well as any part state frame data if
				// the current frame is within the list size.
				mergeJson(properties, partState.properties)

				for ((key, props) in partState.frameProperties) {
					if (props.size() > frame) {
						properties[key] = props.get(frame).deepCopy()
					}
				}

				// Each part can only have one state type x state match, so we are done.
				break
			}
		}
	}

	private class RotationGroup {
		var angularVelocity = 0.0
		var rotationCenter = Vector2d.ZERO
		val targetAngle = networkedFloat()
		var currentAngle = 0.0
		val immediateEvent = networkedEventCounter()

		fun tick(delta: Double) {
			if (angularVelocity == 0.0) {
				currentAngle = targetAngle.get()
			} else {
				currentAngle = approachAngle(targetAngle.get(), currentAngle, angularVelocity * delta)
			}
		}
	}

	private class TransformationGroup {
		private val elements = ArrayList<NetworkedElement>()

		fun addTo(group: NetworkedGroup) {
			elements.forEach { group.add(it) }
		}

		var interpolated = false

		var xTranslation by networkedFloat(0.0).also { elements.add(it); it.interpolator = Interpolator.Linear }
		var yTranslation by networkedFloat(0.0).also { elements.add(it); it.interpolator = Interpolator.Linear }
		var xScale by networkedFloat(1.0).also { elements.add(it); it.interpolator = Interpolator.Linear }
		var yScale by networkedFloat(1.0).also { elements.add(it); it.interpolator = Interpolator.Linear }
		var xShear by networkedFloat(0.0).also { elements.add(it); it.interpolator = Interpolator.Linear }
		var yShear by networkedFloat(0.0).also { elements.add(it); it.interpolator = Interpolator.Linear }

		fun affineTransform(): Matrix3f {
			return Matrix3f.rowMajor(
				(xScale * cos(xShear)).toFloat(), (xScale * sin(xShear)).toFloat(), xTranslation.toFloat(),
				(yScale * sin(yShear)).toFloat(), (yScale * cos(yShear)).toFloat(), yTranslation.toFloat(),
				0f, 0f, 1f
			)
		}

		fun setAffineTransform(value: Matrix3f) {
			xTranslation = value[2, 0].toDouble()
			yTranslation = value[2, 1].toDouble()

			xScale = sqrt(value[0, 0].toDouble() * value[0, 0].toDouble() + value[1, 0].toDouble() * value[1, 0].toDouble())
			yScale = sqrt(value[0, 1].toDouble() * value[0, 1].toDouble() + value[1, 1].toDouble() * value[1, 1].toDouble())

			xShear = atan2(value[1, 0].toDouble(), value[0, 0].toDouble())
			yShear = atan2(value[0, 1].toDouble(), value[1, 1].toDouble())
		}

		fun reset() {
			xTranslation = 0.0
			yTranslation = 0.0
			xScale = 1.0
			yScale = 1.0
			xShear = 0.0
			yShear = 0.0
		}
	}

	private class ParticleEmitter {
		data class Config(val count: Int, val offset: Vector2d, val flip: Boolean, val factory: ParticleFactory)

		private val elements = ArrayList<NetworkedElement>()

		fun addTo(group: NetworkedGroup) {
			elements.forEach { group.add(it) }
		}

		var emissionRate by networkedFloat().also { elements.add(it) }
		var burstCount by networkedUnsignedInt().also { elements.add(it) }
		var randomSelectCount by networkedUnsignedInt().also { elements.add(it) }
		var emissionRateVariance = 0.0
		var offsetRegion by networkedAABBNullable().also { elements.add(it) }
		var anchorPart: String? = null
		var transformationGroups: List<String> = listOf()
		var rotationGroup: String? = null
		var rotationCenter: Vector2d? = null

		val particleList = ArrayList<Config>()

		var active by networkedBoolean().also { elements.add(it) }
		val burstEvent = networkedEventCounter().also { elements.add(it); it.ignoreOccurrencesOnLoad = true }

		var timer = 0.0
	}

	val networkGroup = NetworkedGroup()

	private val elements = ArrayList<NetworkedElement>()

	var processingDirectives by networkedString().also { elements.add(it) }
	var zoom by networkedFloat(1.0).also { elements.add(it) }
	var isFlipped by networkedBoolean().also { elements.add(it) }
	var flippedRelativeCenterLine by networkedFloat().also { elements.add(it) }
	var animationRate by networkedFloat(1.0).also { elements.add(it); it.interpolator = Interpolator.Linear }

	private val globalTags = NetworkedMap(InternedStringCodec, InternedStringCodec)
	private val parts = Object2ObjectAVLTreeMap<String, Part>()
	private val partTags = HashMap<String, NetworkedMap<String, String>>()

	// iterate by priority, network by sorted key
	private val stateTypes = LinkedHashMap<String, StateType>()
	private val rotationGroups = Object2ObjectAVLTreeMap<String, RotationGroup>()
	private val transformationGroups = Object2ObjectAVLTreeMap<String, TransformationGroup>()
	private val particleEmitters = Object2ObjectAVLTreeMap<String, ParticleEmitter>()
	private val lights = Object2ObjectAVLTreeMap<String, Light>()
	private val sounds = Object2ObjectAVLTreeMap<String, Sound>()
	private val effects = Object2ObjectAVLTreeMap<String, Effect>()

	private val random = random()

	init {
		setupNetworkElements()
	}

	constructor(config: AnimationDefinition) : this() {
		for ((k, v) in config.globalTagDefaults) {
			globalTags[k] = v
		}

		for ((part, tags) in config.partTagDefaults) {
			for ((k, v) in tags) {
				partTags.computeIfAbsent(part) { NetworkedMap(InternedStringCodec, InternedStringCodec) }.put(k, v)
			}
		}

		for ((k, v) in config.transformationGroups) {
			val group = TransformationGroup()
			transformationGroups[k] = group
			group.interpolated = v.interpolated
		}

		for ((k, v) in config.rotationGroups) {
			val group = RotationGroup()
			group.angularVelocity = v.angularVelocity
			group.rotationCenter = v.rotationCenter
			rotationGroups[k] = group
		}

		for ((k, v) in config.particleEmitters) {
			val emitter = ParticleEmitter()

			emitter.emissionRate = v.emissionRate
			emitter.emissionRateVariance = v.emissionRateVariance
			emitter.offsetRegion = KOptional.ofNullable(v.offsetRegion)
			emitter.anchorPart = v.anchorPart
			emitter.transformationGroups = v.transformationGroups
			emitter.rotationGroup = v.rotationGroup
			emitter.rotationCenter = v.rotationCenter
			emitter.burstCount = v.burstCount
			emitter.randomSelectCount = v.randomSelectCount
			emitter.active = v.active

			for (particle in v.particles) {
				val factory = particle.particle.map({ it.value }, { it }) ?: continue // not a valid particle, too bad.
				emitter.particleList.add(ParticleEmitter.Config(particle.count, particle.offset, particle.flip, factory))
			}

			particleEmitters[k] = emitter
		}

		for ((k, v) in config.lights) {
			val light = Light()

			light.active = v.active
			light.xPosition = v.position.x
			light.yPosition = v.position.y
			light.color = v.color
			light.anchorPart = v.anchorPart
			light.transformationGroups = v.transformationGroups
			light.rotationGroup = v.rotationGroup
			light.rotationCenter = v.rotationCenter
			light.pointAngle = Math.toRadians(v.pointAngle)
			light.pointLight = v.pointLight
			light.pointBeam = v.pointBeam
			light.beamAmbience = v.beamAmbience

			if (v.flickerPeriod != null)
				light.flicker = PeriodicFunction(v.flickerPeriod, v.flickerMinIntensity, v.flickerMaxIntensity, v.flickerPeriodVariance, v.flickerIntensityVariance)

			lights[k] = light
		}

		for ((k, v) in config.sounds) {
			val sound = Sound()

			if (v.isLeft) {
				sound.soundPool.addAll(v.left())
			} else {
				val conf = v.right()
				sound.soundPool.addAll(conf.pool.map { it.fullPath })
				sound.xPosition = conf.position.x
				sound.yPosition = conf.position.y
				sound.volumeTarget = conf.volume
				sound.volumeRampTime = conf.volumeRampTime
				sound.pitchMultiplierTarget = conf.pitchMultiplier
				sound.pitchMultiplierRampTime = conf.pitchMultiplierRampTime
				sound.rangeMultiplier = conf.rangeMultiplier
			}

			sounds[k] = sound
		}

		for ((k, v) in config.effects) {
			effects[k] = Effect(v.type, v.time, v.directives)
		}

		if (config.animatedParts != null) {
			for ((k, v) in config.animatedParts.stateTypes.entries.sortedWith { o1, o2 -> o2.value.priority.compareTo(o1.value.priority) }) {
				stateTypes[k] = StateType(v)
			}

			for ((k, v) in config.animatedParts.parts) {
				parts[k] = Part(v)
			}
		}

		for (k in parts.keys) {
			partTags.computeIfAbsent(k) { NetworkedMap(InternedStringCodec, InternedStringCodec) }
		}

		setupNetworkElements()
	}

	// Every part image can have one or more <tag> directives in it, which if set
	// here will be replaced by the tag value when constructing Drawables.  All
	// Drawables can also have a <frame> tag which will be set to whatever the
	// current state frame is (1 indexed, so the first frame is 1).
	fun setGlobalTag(key: String, value: String) {
		globalTags[key] = value
	}

	fun setPartTag(partName: String, tagKey: String, tagValue: String) {
		partTags.computeIfAbsent(partName) {
			LOGGER.warn("Creating part tags for $it after initialization, this can cause client-server desyncs")
			NetworkedMap(InternedStringCodec, InternedStringCodec)
		}.put(tagKey, tagValue)
	}

	private fun setupNetworkElements() {
		networkGroup.clear()
		elements.forEach { networkGroup.add(it) }

		networkGroup.add(globalTags)

		// animated part set
		for (v in parts.keys) {
			networkGroup.add(partTags[v] ?: throw RuntimeException("Missing animated part $v!"))
		}

		stateTypes.entries.stream()
			.sorted { o1, o2 -> o1.key.compareTo(o2.key) }
			.map { it.value }
		.forEach {
			networkGroup.add(it.stateIndex)
			networkGroup.add(it.startedEvent)
		}

		for (v in transformationGroups.values) {
			v.addTo(networkGroup)
		}

		for (v in rotationGroups.values) {
			networkGroup.add(v.targetAngle)
			networkGroup.add(v.immediateEvent)
		}

		for (v in particleEmitters.values) {
			v.addTo(networkGroup)
		}

		for (v in lights.values) {
			v.addTo(networkGroup)
		}

		for (v in sounds.values) {
			v.addTo(networkGroup)
		}

		for (v in effects.values) {
			networkGroup.add(v.enabled)
		}
	}

	fun setActiveState(type: String, state: String, alwaysStart: Boolean = false): Boolean {
		return stateTypes[type]?.set(state, alwaysStart) ?: return false
	}

	fun animationState(type: String): String {
		val get = stateTypes[type] ?: throw NoSuchElementException("No such animation part $type")
		return get.activeState?.name ?: "" // original engine parity...............
		// it will return empty string if state type has no active state (
	}

	fun stateProperty(type: String, path: String): JsonElement {
		val get = stateTypes[type] ?: throw NoSuchElementException("No such animation part $type")
		return get.activeState?.properties?.get(path) ?: JsonNull.INSTANCE // очень и очень грустно
	}

	fun stateProperty(type: String, path: JsonPath): JsonElement {
		val get = stateTypes[type] ?: throw NoSuchElementException("No such animation part $type")
		val props = get.activeState?.properties ?: return JsonNull.INSTANCE
		return path.find(props) ?: JsonNull.INSTANCE // очень и очень грустно
	}

	fun rotateGroup(group: String, targetAngle: Double, immediate: Boolean = false) {
		val get = rotationGroups[group] ?: throw NoSuchElementException("No such rotation group $group")
		get.targetAngle.accept(targetAngle)

		if (immediate) {
			get.currentAngle = targetAngle
			get.immediateEvent.trigger()
		}
	}

	fun currentRotationAngle(group: String): Double {
		return rotationGroups[group]?.currentAngle ?: throw NoSuchElementException("No such rotation group $group")
	}

	fun targetRotationAngle(group: String): Double {
		return rotationGroups[group]?.targetAngle?.get() ?: throw NoSuchElementException("No such rotation group $group")
	}

	fun hasRotationGroup(group: String): Boolean {
		return group in rotationGroups
	}

	fun rotationGroups(): Collection<String> {
		return Collections.unmodifiableCollection(rotationGroups.keys)
	}

	fun transformationGroups(): Collection<String> {
		return Collections.unmodifiableCollection(transformationGroups.keys)
	}

	fun hasTransformationGroup(group: String): Boolean {
		return group in transformationGroups
	}

	fun translateTransformGroup(group: String, translation: Vector2f) {
		val get = transformationGroups[group] ?: throw NoSuchElementException("No such transformation group $group")
		get.setAffineTransform(Matrix3f.rowMajor(r02 = translation.x, r12 = translation.y).mul(get.affineTransform()))
	}

	fun rotateTransformGroup(group: String, rotation: Float, center: Vector2f = Vector2f.ZERO) {
		val get = transformationGroups[group] ?: throw NoSuchElementException("No such transformation group $group")
		val sin = sin(rotation)
		val cos = cos(rotation)

		val matrix = Matrix3f.rowMajor(
			cos, -sin, center.x - cos * center.x + sin * center.y,
			sin,  cos, center.y - sin * center.y - cos * center.x
		)

		get.setAffineTransform(matrix.mul(get.affineTransform()))
	}

	fun scaleTransformationGroup(group: String, scale: Vector2f, center: Vector2f = Vector2f.ZERO) {
		val get = transformationGroups[group] ?: throw NoSuchElementException("No such transformation group $group")

		val matrix = Matrix3f.rowMajor(
			scale.x, 0f, center.x - center.x * scale.x,
			0f, scale.y, center.y - center.y * scale.y
		)

		get.setAffineTransform(matrix.mul(get.affineTransform()))
	}

	fun transformTransformationGroup(
		group: String,
		r00: Float,
		r01: Float,
		r02: Float,
		r10: Float,
		r11: Float,
		r12: Float,
	) {
		val get = transformationGroups[group] ?: throw NoSuchElementException("No such transformation group $group")
		val matrix = Matrix3f.rowMajor(r00, r01, r02, r10, r11, r12)
		get.setAffineTransform(matrix.mul(get.affineTransform()))
	}

	fun resetTransformationGroup(group: String) {
		val get = transformationGroups[group] ?: throw NoSuchElementException("No such transformation group $group")
		get.reset()
	}

	fun hasParticleEmitter(emitter: String): Boolean {
		return emitter in particleEmitters
	}

	fun particleEmitters(): Collection<String> {
		return Collections.unmodifiableCollection(particleEmitters.keys)
	}

	fun setParticleEmitterActive(emitter: String, state: Boolean = true) {
		val get = particleEmitters[emitter] ?: throw NoSuchElementException("No such particle emitter $emitter")
		get.active = state
	}

	fun setParticleEmitterEmissionRate(emitter: String, rate: Double) {
		val get = particleEmitters[emitter] ?: throw NoSuchElementException("No such particle emitter $emitter")
		get.emissionRate = rate
	}

	fun setParticleEmitterBurstCount(emitter: String, count: Int) {
		val get = particleEmitters[emitter] ?: throw NoSuchElementException("No such particle emitter $emitter")
		require(count >= 0) { "Negative burst count: $count" }
		get.burstCount = count
	}

	fun setParticleEmitterOffsetRegion(emitter: String, region: AABB) {
		val get = particleEmitters[emitter] ?: throw NoSuchElementException("No such particle emitter $emitter")
		get.offsetRegion = KOptional(region)
	}

	fun setParticleEmitterOffsetRegion(emitter: String) {
		val get = particleEmitters[emitter] ?: throw NoSuchElementException("No such particle emitter $emitter")
		get.offsetRegion = KOptional()
	}

	fun burstParticleEmitter(emitter: String) {
		val get = particleEmitters[emitter] ?: throw NoSuchElementException("No such particle emitter $emitter")
		get.burstEvent.trigger()
	}

	fun lights(): Collection<String> {
		return Collections.unmodifiableCollection(lights.keys)
	}

	fun hasLight(light: String): Boolean {
		return light in lights
	}

	fun setLightActive(light: String, state: Boolean = true) {
		val get = lights[light] ?: throw NoSuchElementException("No such light source $light")
		get.active = state
	}

	fun setLightPosition(light: String, position: Vector2d) {
		val get = lights[light] ?: throw NoSuchElementException("No such light source $light")
		get.xPosition = position.x
		get.yPosition = position.y
	}

	fun setLightPosition(light: String, x: Double, y: Double) {
		val get = lights[light] ?: throw NoSuchElementException("No such light source $light")
		get.xPosition = x
		get.yPosition = y
	}

	fun setLightColor(light: String, color: RGBAColor) {
		val get = lights[light] ?: throw NoSuchElementException("No such light source $light")
		get.color = color
	}

	fun setLightPointAngle(light: String, angle: Double) {
		val get = lights[light] ?: throw NoSuchElementException("No such light source $light")
		get.pointAngle = angle
	}

	fun sounds(): Collection<String> {
		return Collections.unmodifiableCollection(sounds.keys)
	}

	fun hasSound(sound: String): Boolean {
		return sound in sounds
	}

	fun setSoundPool(sound: String, pool: Collection<String>) {
		val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound")

		get.soundPool.clear()
		get.soundPool.addAll(pool)
	}

	fun setSoundPosition(sound: String, position: Vector2d) {
		val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound")

		get.xPosition = position.x
		get.yPosition = position.y
	}

	fun setSoundPosition(sound: String, x: Double, y: Double) {
		val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound")

		get.xPosition = x
		get.yPosition = y
	}

	fun playSound(sound: String, loops: Int = 0) {
		val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound")
		require(loops >= 0) { "Negative amount of loops: $loops" }
		get.loops = loops
		get.signals.push(SoundSignal.PLAY)
	}

	fun setSoundVolume(sound: String, volume: Double, rampTime: Double = 0.0) {
		val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound")
		get.volumeTarget = volume
		get.volumeRampTime = rampTime
	}

	fun setSoundPitch(sound: String, pitch: Double, rampTime: Double = 0.0) {
		val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound")
		get.pitchMultiplierTarget = pitch
		get.pitchMultiplierRampTime = rampTime
	}

	fun stopAllSounds(sound: String, rampTime: Double = 0.0) {
		val get = sounds[sound] ?: throw NoSuchElementException("No such sound source $sound")
		get.volumeRampTime = rampTime
		get.signals.push(SoundSignal.STOP_ALL)
	}

	fun effects(): Collection<String> {
		return Collections.unmodifiableCollection(effects.keys)
	}

	fun hasEffect(effect: String): Boolean {
		return effect in effects
	}

	fun setEffectActive(effect: String, state: Boolean = true) {
		val get = effects[effect] ?: throw NoSuchElementException("No such effect source $effect")
		get.enabled.accept(state)
	}

	fun parts(): Collection<String> {
		return Collections.unmodifiableCollection(parts.keys)
	}

	fun partPoint(part: String, property: String): Vector2d? {
		val get = parts[part] ?: throw NoSuchElementException("No such animation part $part")
		get.freshen()
		val lookup = get.properties[property] ?: return null
		return vectors.fromJsonTree(lookup)
	}

	fun partPoly(part: String, property: String): Poly? {
		val get = parts[part] ?: throw NoSuchElementException("No such animation part $part")
		get.freshen()
		val lookup = get.properties[property] ?: return null
		return polies.fromJsonTree(lookup)
	}

	fun partProperty(part: String, property: String): JsonElement {
		val get = parts[part] ?: throw NoSuchElementException("No such animation part $part")
		get.freshen()
		return get.properties[property] ?: JsonNull.INSTANCE
	}

	fun partTransformation(part: String): Matrix3d {
		val get = parts[part] ?: throw NoSuchElementException("No such animation part $part")
		val result = Matrix3d.rowMajor()

		val active = get.activeState ?: return result
		// TODO
		return result
	}

	// TODO: Dynamic target
	@Suppress("Name_Shadowing")
	fun tick(delta: Double) {
		val delta = delta * animationRate

		for (state in stateTypes.values) {
			state.tick(delta)

			if ("lightsOn" in state.activeProperties) {
				for (name in state.activeProperties.getArray("lightsOn")) {
					lights[name.asString]?.active = true
				}
			}

			if ("lightsOff" in state.activeProperties) {
				for (name in state.activeProperties.getArray("lightsOff")) {
					lights[name.asString]?.active = false
				}
			}

			if ("particleEmittersOn" in state.activeProperties) {
				for (name in state.activeProperties.getArray("particleEmittersOn")) {
					particleEmitters[name.asString]?.active = true
				}
			}

			if ("particleEmittersOff" in state.activeProperties) {
				for (name in state.activeProperties.getArray("particleEmittersOff")) {
					particleEmitters[name.asString]?.active = false
				}
			}
		}

		parts.values.forEach { it.activePartDirty = true }

		for (rotationGroup in rotationGroups.values) {
			rotationGroup.tick(delta)
		}

		for (light in lights.values) {
			light.flicker?.update(delta, random)
		}

		for (effect in effects.values) {
			effect.tick(delta)
		}
	}

	fun finishAnimations() {
		for (state in stateTypes.values) {
			state.finishAnimations()
		}

		parts.values.forEach { it.activePartDirty = true }
	}

	companion object {
		// lame
		fun load(path: String): Animator {
			val json = Starbound.loadJsonAsset(path)

			if (json == null) {
				if (missing.add(path)) {
					LOGGER.error("Unable to instance Animator from $path! This very likely gonna make networking code go haywire.")
				}

				return Animator()
			}

			return Animator(Starbound.gson.fromJson(json, AnimationDefinition::class.java))
		}

		private val LOGGER = LogManager.getLogger()
		private val missing = Collections.synchronizedSet(ObjectOpenHashSet<String>())
		private val vectors by lazy { Starbound.gson.getAdapter(Vector2d::class.java) }
		private val polies by lazy { Starbound.gson.getAdapter(Poly::class.java) }
	}
}
