package ru.dbotthepony.kstarbound.world.entities

import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.collect.ListenableMap
import ru.dbotthepony.kommons.io.IntValueCodec
import ru.dbotthepony.kommons.io.KOptionalIntValueCodec
import ru.dbotthepony.kommons.io.KOptionalVarIntValueCodec
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.UnsignedVarIntCodec
import ru.dbotthepony.kommons.io.readKOptional
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeKOptional
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.util.value
import ru.dbotthepony.kstarbound.collect.IdMap
import ru.dbotthepony.kstarbound.defs.actor.StatModifier
import ru.dbotthepony.kstarbound.defs.actor.StatModifierType
import ru.dbotthepony.kstarbound.defs.actor.StatusControllerConfig
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.network.syncher.NetworkedDynamicGroup
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedDouble
import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedJsonObject
import ru.dbotthepony.kstarbound.network.syncher.networkedString
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.Collections
import java.util.function.LongSupplier
import kotlin.properties.Delegates

// this is unnatural to have this class separated, but since it contains
// lots of internal state, it would be nice to have it encapsulated;
// despite this class being used exactly once - as part of ActorEntity.

// On other hand, original code separates StatCollection into subclass,
// and we won't do this.

// On side note, it appears game initially separated StatSet from StatCollection,
// which means early in development Starbound had true singleplayer, which eventually
// got removed in favor of listening server.
// Unfortunately for me, this means I have to untangle this piece of code
// and unify StatSet and StatCollection logic into one thing.

// StatSet original docs:
// Manages a collection of Stats and Resources.
//
// Stats are named floating point values of any base value, with an arbitrary
// number of "stat modifiers" attached to them.  Stat modifiers can be added
// and removed in groups, and they can either raise or lower stats by a
// constant value or a percentage of the stat value without any other
// percentage modifications applied.  The effective stat value is always the
// value with all mods applied.  If a modifier is created for a stat that does
// not exist, there will be an effective stat value for the modified stat, but
// NO base stat.  If the modifier is a base percentage modifier, it will have
// no effect because it is assumed that base stats that do not exist are zero.
//
// Resources are also named floating point values, but are in a different
// namespaced and are intended to be used as values that change regularly.
// They are always >= 0.0f, and optionally have a maximum value based on a
// given value or stat.  In addition to a max value, they can also have a
// "delta" value or stat, which automatically adds or removes that delta to the
// resource every second.
//
// If a resource has a maximum value, then rather than trying to keep the
// *value* of the resource constant, this class will instead attempt to keep
// the *percentage* of the resource constant across stat changes.  For example,
// if "health" is a stat with a max of 100, and the current health value is 50,
// and the max health stat is changed to 200 through any means, the health
// value will automatically update to 100.

// StatCollection original docs:
// Extension of StatSet that can easily be set up from config, and is network
// capable.
class StatusController(val entity: ActorEntity, val config: StatusControllerConfig) : NetworkedElement.Passthrough() {
	// status effects
	private val networkGroup = NetworkedGroup()
	private val statNetworkGroup = NetworkedGroup()

	override val parentElement: NetworkedElement
		get() = networkGroup

	init {
		networkGroup.add(statNetworkGroup)
	}

	private var statusProperties by networkedJsonObject(config.statusProperties).also { networkGroup.add(it) }

	var parentDirectives by networkedString().also { networkGroup.add(it) }
		private set

	private val uniqueEffectMetadata = NetworkedDynamicGroup(::UniqueEffectMetadata, UniqueEffectMetadata::networkGroup).also { networkGroup.add(it) }
	private val effectAnimators = NetworkedDynamicGroup(::EffectAnimator, { it }).also { networkGroup.add(it) }

	fun update(delta: Double) {
		updateStats(delta)
	}

	private class EffectAnimator(var config: KOptional<String> = KOptional()) : Passthrough() {
		var animator = Animator()
			private set

		override val parentElement: NetworkedElement
			get() = animator.networkGroup

		override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
			val old = config
			config = data.readKOptional { readInternedString() }

			if (old != config)
				animator = config.map { Animator.load(it) }.orElse { Animator() }

			super.readInitial(data, isLegacy)
		}

		override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
			data.writeKOptional(config) { writeBinaryString(it) }
			super.writeInitial(data, isLegacy)
		}
	}

	private class UniqueEffectMetadata {
		val networkGroup = NetworkedGroup()

		var duration by networkedFixedPoint(0.01).also { networkGroup.add(it); it.interpolator = Interpolator.Linear }
		var maxDuration by networkedFloat().also { networkGroup.add(it) }
		var sourceEntity by networkedData(KOptional(), KOptionalIntValueCodec).also { networkGroup.add(it) }
	}

	// stats
	sealed class LiveStat {
		abstract val baseValue: Double

		// Value with just the base percent modifiers applied and the value
		// modifiers
		abstract val baseModifiedValue: Double

		// Final modified value that includes the effective modifiers.
		abstract val effectiveModifiedValue: Double
	}

	private class LiveStatImpl : LiveStat() {
		override var baseValue: Double = 0.0

		// Value with just the base percent modifiers applied and the value
		// modifiers
		override var baseModifiedValue: Double = 0.0

		// Final modified value that includes the effective modifiers.
		override var effectiveModifiedValue: Double = 0.0
	}

	sealed class Resource {
		// null means no limit
		abstract val max: Either<String, Double>?
		abstract val delta: Either<String, Double>?

		abstract val name: String

		abstract var isLocked: Boolean
		abstract var value: Double

		abstract val maxValue: Double?

		fun setAsPercentage(percent: Double) {
			val maxValue = maxValue

			if (maxValue == null) {
				if (percent == 0.0) {
					LOGGER.warn("Preserving odd quirk of original engine: $name requires resource $max to be present to be set as 0% of that, however, $max is missing. Due to flawed `if` branch in original engine, `initialPercentage` specified as `0` falls through condition and is treated as `initialValue`=`0`")
				} else {
					throw IllegalArgumentException("$name does not have max value")
				}
			} else {
				this.value = maxValue * percent
			}
		}
	}

	private class ResourceImpl(
		override val name: String,
		override val max: Either<String, Double>?,
		override val delta: Either<String, Double>?
	) : Resource() {
		val actualValue = networkedFloat()

		override var value: Double
			get() = actualValue.value
			set(value) {
				val maxValue = maxValue

				if (maxValue == null) {
					actualValue.value = value
				} else {
					actualValue.value = value.coerceAtMost(maxValue)
				}
			}

		val actualIsLocked = networkedBoolean()

		override var isLocked: Boolean
			get() = actualIsLocked.value
			set(value) { actualIsLocked.value = value }

		override var maxValue: Double? = null

		private var defaultValue: Double = 0.0

		fun mark() {
			defaultValue = value
		}

		fun reset() {
			value = defaultValue
		}
	}

	// in original code it is named effectiveStats
	private val liveStatsInternal = HashMap<String, LiveStatImpl>()

	private val statModifiersMap = NetworkedMap(
		IntValueCodec,
		NATIVE_MODIFIERS_CODEC to LEGACY_MODIFIERS_CODEC,
		map = ListenableMap(Int2ObjectAVLTreeMap()))

	private val statModifiers = IdMap(map = statModifiersMap)
	private val resourcesInternal = HashMap<String, ResourceImpl>()

	val liveStats: Map<String, LiveStat> = Collections.unmodifiableMap(liveStatsInternal)
	val resources: Map<String, Resource> = Collections.unmodifiableMap(resourcesInternal)

	init {
		for ((statName, stat) in config.stats) {
			val live = LiveStatImpl()
			liveStatsInternal[statName] = live
			live.baseValue = stat.baseValue
			live.baseModifiedValue = stat.baseValue
		}

		for ((resourceName, res) in config.resources) {
			resourcesInternal[resourceName] = ResourceImpl(
				resourceName,
				max = res.maxValue?.let { Either.right(it) } ?: res.maxStat?.let { Either.left(it) },
				delta = res.deltaValue?.let { Either.right(it) } ?: res.deltaStat?.let { Either.left(it) },
			)
		}

		updateStats(0.0)

		for ((resourceName, res) in config.resources) {
			val resource = resourcesInternal[resourceName]!!

			if (res.initialValue != null) {
				resource.value = res.initialValue
			} else if (res.initialPercentage != null) {
				resource.setAsPercentage(res.initialPercentage)
			} else if (resource.maxValue != null) {
				resource.setAsPercentage(1.0)
			}

			resource.mark()
		}

		statNetworkGroup.add(statModifiersMap)

		for (k in resourcesInternal.keys.sorted()) {
			val resource = resourcesInternal[k]!!

			statNetworkGroup.add(resource.actualValue)
			statNetworkGroup.add(resource.actualIsLocked)
		}
	}

	fun resetResources() {
		for (resource in resourcesInternal.values) {
			resource.reset()
		}
	}

	fun addStatModifiers(modifiers: Collection<StatModifier>): Int {
		return statModifiers.add(ArrayList(modifiers))
	}

	fun removeStatModifiers(index: Int): Boolean {
		return statModifiers.remove(index) != null
	}

	private fun updateStats(delta: Double) {
		// We use two intermediate values for calculating the effective stat value.
		// The baseModifiedValue represents the application of the base percentage
		// modifiers and the value modifiers, which only depend on the baseValue.
		// The effectiveModifiedValue is the application of all effective percentage
		// modifiers successively on the baseModifiedValue, causing them to stack with
		// each other in addition to base multipliers and value modifiers

		val neverVisited = ObjectArraySet(liveStats.keys)

		for ((statName, stat) in config.stats) {
			val live = liveStatsInternal[statName]!!
			live.baseValue = stat.baseValue
			live.baseModifiedValue = stat.baseValue
			neverVisited.remove(statName)
		}

		for (group in statModifiers.values) {
			for (modifier in group) {
				var live = liveStatsInternal[modifier.stat]

				if (live == null) {
					live = LiveStatImpl()
					liveStatsInternal[modifier.stat] = live
				}

				neverVisited.remove(modifier.stat)

				if (modifier.type == StatModifierType.BASE_MULTIPLICATION) {
					live.baseModifiedValue += (modifier.value - 1.0) * live.baseValue
				} else if (modifier.type == StatModifierType.BASE_ADDITION) {
					live.baseModifiedValue += modifier.value
				}
			}
		}

		// Then we do all the StatEffectiveMultipliers and compute the
		// final effectiveModifiedValue

		for (value in liveStatsInternal.values)
			value.effectiveModifiedValue = value.baseModifiedValue

		for (group in statModifiers.values) {
			for (modifier in group) {
				val live = liveStatsInternal[modifier.stat]!!

				if (modifier.type == StatModifierType.OVERALL_MULTIPLICATION) {
					live.effectiveModifiedValue *= modifier.value
				} else if (modifier.type == StatModifierType.OVERALL_ADDITION) {
					live.effectiveModifiedValue += modifier.value
				}
			}
		}

		for (value in neverVisited) {
			val live = liveStatsInternal[value]!!

			live.baseValue = 0.0
			live.effectiveModifiedValue = 0.0
			live.baseModifiedValue = 0.0
		}

		// Then update all the resources due to charging and percentage tracking,
		// after updating the stats.

		for (resource in resourcesInternal.values) {
			val oldMax = resource.maxValue
			resource.maxValue = resource.max?.map({ liveStatsInternal[it]?.effectiveModifiedValue }, { it })

			// If the resource has a maximum value, rather than keeping the absolute
			// value of the resource the same between updates, the resource value
			// should instead track the percentage.
			if (oldMax != null && resource.maxValue != null && resource.maxValue!! > 0.0) {
				resource.value *= resource.maxValue!! / oldMax
			}

			if (resource.maxValue != null) {
				resource.value = resource.value
			}

			if (delta > 0.0) {
				resource.value += delta * (resource.delta?.map({ liveStatsInternal[it]?.effectiveModifiedValue }, { it }) ?: 0.0)
			}
		}
	}

	companion object {
		private val LEGACY_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.LEGACY_CODEC, ::ArrayList)
		private val NATIVE_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.CODEC, ::ArrayList)
		private val LOGGER = LogManager.getLogger()
	}
}
