package ru.dbotthepony.mc.otm.network.syncher

import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
import net.minecraft.core.RegistryAccess
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.world.item.ItemStack
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.collect.ListenableMap
import ru.dbotthepony.kommons.collect.ListenableSet
import ru.dbotthepony.kommons.util.Delegate
import ru.dbotthepony.kommons.util.DelegateGetter
import ru.dbotthepony.kommons.util.DelegateSetter
import ru.dbotthepony.kommons.util.Listenable
import ru.dbotthepony.kommons.util.ListenableDelegate
import ru.dbotthepony.kommons.util.Observer
import ru.dbotthepony.mc.otm.OTM_CLEANER
import ru.dbotthepony.mc.otm.core.collect.filterNotNull
import ru.dbotthepony.mc.otm.core.collect.map
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.network.ItemStackCodec
import ru.dbotthepony.mc.otm.network.MatteryStreamCodec
import ru.dbotthepony.mc.otm.network.StreamCodecs
import ru.dbotthepony.mc.otm.network.decodePayload
import ru.dbotthepony.mc.otm.network.encodePayload
import ru.dbotthepony.mc.otm.network.wrap
import java.io.Closeable
import java.lang.ref.Cleaner.Cleanable
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.ReentrantLock
import java.util.function.BooleanSupplier
import java.util.function.DoubleSupplier
import java.util.function.IntSupplier
import java.util.function.LongSupplier
import java.util.function.Supplier
import kotlin.collections.ArrayList
import kotlin.concurrent.withLock

/**
 * Universal, one-to-many delegate/value synchronizer.
 *
 * Values and delegates can be attached using [add];
 * Delta changes are tracked by [SynchableGroup.Remote] instances.
 *
 * Attached delegates can be safely mutated by multiple threads concurrently
 * (attached [ListenableDelegate] can call [Listenable.addListener] callback from different threads).
 *
 * Generally, this class is considered to be "root" of [ISynchable] operation (due to API), but this is not strictly necessary,
 * and [SynchableGroup] can be as well embedded into other [SynchableGroup]s.
 */
@Suppress("UNUSED")
class SynchableGroup : Observer, ISynchable, Iterable<ISynchable> {
	private val slots = ArrayList<Slot?>()
	private val gaps = IntAVLTreeSet()
	private val observers = ArrayList<Slot>()
	private val remotes = ArrayList<Remote>()
	override var isRemote = false
		private set

	override val hasRemotes: Boolean
		get() = remotes.isNotEmpty()

	val hasChildren: Boolean
		get() = slots.any { it != null }

	override fun iterator(): Iterator<ISynchable> {
		return slots.iterator().map { it?.synchable }.filterNotNull()
	}

	override fun observe(): Boolean {
		var any = false
		observers.forEach { any = it.observe() || any }
		return any
	}

	override val shouldBeObserved: Boolean
		get() = true

	override fun createRemoteState(listener: Runnable): IRemoteState {
		return Remote(listener)
	}

	override fun read(stream: RegistryFriendlyByteBuf) {
		isRemote = true

		var readID = stream.readVarInt()

		while (readID > 0) {
			val slot = slots.getOrNull(readID - 1) ?: throw IndexOutOfBoundsException("Unknown networked slot ${readID - 1}!")
			slot.synchable.read(stream)
			readID = stream.readVarInt()
		}
	}

	fun read(registry: RegistryAccess, bytes: ByteArrayList) {
		return decodePayload(registry, bytes) {
			read(it)
		}
	}

	internal inner class Slot(val synchable: ISynchable) : Closeable, Observer {
		val id: Int

		private var isRemoved = false
		val remoteSlots = ArrayList<Remote.RemoteSlot>()

		override fun close() {
			if (!isRemoved) {
				isRemoved = true
				slots[id] = null
				gaps.add(id)

				observers.remove(this)
				remoteSlots.toTypedArray().forEach { it.remove() }
			}
		}

		override fun observe(): Boolean {
			return synchable.observe()
		}

		private fun markDirty() {
			if (!isRemote && !isRemoved) {
				remoteSlots.forEach { it.markDirty() }
			}
		}

		init {
			if (synchable.shouldBeObserved) {
				observers.add(this)
			}

			if (gaps.isNotEmpty()) {
				val gap = gaps.firstInt()
				gaps.remove(gap)
				id = gap
				check(slots[id] == null) { "Expected slot $id to be empty" }
				slots[id] = this
			} else {
				id = slots.size
				slots.add(this)
			}

			remotes.forEach {
				it.addSlot(this)
			}
		}
	}

	fun add0(synchable: ISynchable): Closeable {
		return Slot(synchable)
	}

	fun <T : ISynchable> add(synchable: T): T {
		Slot(synchable)
		return synchable
	}

	fun <V> add(value: V, codec: MatteryStreamCodec<RegistryFriendlyByteBuf, V>, setter: DelegateSetter<V> = DelegateSetter.passthrough(), getter: DelegateGetter<V> = DelegateGetter.passthrough()): SynchableDelegate<V> {
		return add(SynchableDelegate(ListenableDelegate.maskSmart(value, getter, setter), codec))
	}

	fun <V> add(delegate: ListenableDelegate<V>, codec: MatteryStreamCodec<RegistryFriendlyByteBuf, V>): SynchableDelegate<V> {
		return add(SynchableDelegate(delegate, codec))
	}

	fun <V> add(delegate: Delegate<V>, codec: MatteryStreamCodec<RegistryFriendlyByteBuf, V>): SynchableObservedDelegate<V> {
		return add(SynchableObservedDelegate(delegate, codec))
	}

	fun <V> add(delegate: Supplier<V>, codec: MatteryStreamCodec<RegistryFriendlyByteBuf, V>): SynchableDelegate<V> {
		return computed(delegate, codec)
	}

	fun <E> add(delegate: ListenableSet<E>, codec: MatteryStreamCodec<RegistryFriendlyByteBuf, E>): SynchableSet<E> {
		return add(SynchableSet(delegate, codec))
	}

	fun <K, V> add(delegate: ListenableMap<K, V>, keyCodec: MatteryStreamCodec<RegistryFriendlyByteBuf, K>, valueCodec: MatteryStreamCodec<RegistryFriendlyByteBuf, V>): SynchableMap<K, V> {
		return add(SynchableMap(delegate, keyCodec, valueCodec))
	}

	fun computedByte(delegate: Supplier<Byte>) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.BYTE))
	fun computedShort(delegate: Supplier<Short>) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.SHORT))
	fun computedInt(delegate: Supplier<Int>) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.INT))
	fun computedLong(delegate: Supplier<Long>) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.VAR_LONG))
	fun computedFloat(delegate: Supplier<Float>) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.FLOAT))
	fun computedDouble(delegate: Supplier<Double>) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.DOUBLE))
	fun computedBoolean(delegate: Supplier<Boolean>) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.BOOLEAN))
	fun computedString(delegate: Supplier<String>) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.STRING))
	fun computedUUID(delegate: Supplier<UUID>) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate), StreamCodecs.UUID))
	fun <V> computed(delegate: Supplier<V>, codec: MatteryStreamCodec<RegistryFriendlyByteBuf, V>) = add(SynchableDelegate(ListenableDelegate.CustomShadow(delegate, codec::compare, codec::copy), codec))

	fun computedInt(delegate: IntSupplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate::getAsInt), StreamCodecs.VAR_INT))
	fun computedLong(delegate: LongSupplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate::getAsLong), StreamCodecs.VAR_LONG))
	fun computedDouble(delegate: DoubleSupplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate::getAsDouble), StreamCodecs.DOUBLE))
	fun computedBoolean(delegate: BooleanSupplier) = add(SynchableDelegate(ListenableDelegate.Shadow(delegate::getAsBoolean), StreamCodecs.BOOLEAN))

	@JvmName("vbyte")
	@JvmOverloads
	fun byte(value: Byte = 0, setter: DelegateSetter<Byte> = DelegateSetter.passthrough(), getter: DelegateGetter<Byte> = DelegateGetter.passthrough()): SynchableDelegate<Byte> {
		return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.BYTE)
	}

	@JvmName("vshort")
	@JvmOverloads
	fun short(value: Short = 0, setter: DelegateSetter<Short> = DelegateSetter.passthrough(), getter: DelegateGetter<Short> = DelegateGetter.passthrough()): SynchableDelegate<Short> {
		return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.SHORT)
	}

	@JvmName("vint")
	@JvmOverloads
	fun int(value: Int = 0, setter: DelegateSetter<Int> = DelegateSetter.passthrough(), getter: DelegateGetter<Int> = DelegateGetter.passthrough()): SynchableDelegate<Int> {
		return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.VAR_INT)
	}

	@JvmName("vlong")
	@JvmOverloads
	fun long(value: Long = 0L, setter: DelegateSetter<Long> = DelegateSetter.passthrough(), getter: DelegateGetter<Long> = DelegateGetter.passthrough()): SynchableDelegate<Long> {
		return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.VAR_LONG)
	}

	@JvmName("vfloat")
	@JvmOverloads
	fun float(value: Float = 0f, setter: DelegateSetter<Float> = DelegateSetter.passthrough(), getter: DelegateGetter<Float> = DelegateGetter.passthrough()): SynchableDelegate<Float> {
		return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.FLOAT)
	}

	@JvmName("vdouble")
	@JvmOverloads
	fun double(value: Double = 0.0, setter: DelegateSetter<Double> = DelegateSetter.passthrough(), getter: DelegateGetter<Double> = DelegateGetter.passthrough()): SynchableDelegate<Double> {
		return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.DOUBLE)
	}

	@JvmName("vboolean")
	@JvmOverloads
	fun boolean(value: Boolean = false, setter: DelegateSetter<Boolean> = DelegateSetter.passthrough(), getter: DelegateGetter<Boolean> = DelegateGetter.passthrough()): SynchableDelegate<Boolean> {
		return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.BOOLEAN)
	}

	@JvmOverloads
	fun string(value: String, setter: DelegateSetter<String> = DelegateSetter.passthrough(), getter: DelegateGetter<String> = DelegateGetter.passthrough()): SynchableDelegate<String> {
		return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.STRING)
	}

	@JvmOverloads
	fun uuid(value: UUID, setter: DelegateSetter<UUID> = DelegateSetter.passthrough(), getter: DelegateGetter<UUID> = DelegateGetter.passthrough()): SynchableDelegate<UUID> {
		return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.UUID)
	}

	@JvmOverloads
	fun <E> set(codec: MatteryStreamCodec<RegistryFriendlyByteBuf, E>, backing: MutableSet<E> = ObjectOpenHashSet()): SynchableSet<E> {
		return add(ListenableSet(backing), codec)
	}

	@JvmOverloads
	fun <K, V> map(keyCodec: MatteryStreamCodec<RegistryFriendlyByteBuf, K>, valueCodec: MatteryStreamCodec<RegistryFriendlyByteBuf, V>, backing: MutableMap<K, V> = Object2ObjectOpenHashMap()): SynchableMap<K, V> {
		return add(ListenableMap(backing), keyCodec, valueCodec)
	}

	@JvmOverloads
	fun <E : Enum<E>> enum(value: E, setter: DelegateSetter<E> = DelegateSetter.passthrough(), getter: DelegateGetter<E> = DelegateGetter.passthrough()): SynchableDelegate<E> {
		return add(ListenableDelegate.maskSmart(value, getter, setter), MatteryStreamCodec.Enum(value::class.java))
	}

	fun decimal(value: Decimal = Decimal.ZERO, setter: DelegateSetter<Decimal> = DelegateSetter.passthrough(), getter: DelegateGetter<Decimal> = DelegateGetter.passthrough()): SynchableDelegate<Decimal> {
		return add(ListenableDelegate.maskSmart(value, getter, setter), StreamCodecs.DECIMAL)
	}

	fun computedDecimal(delegate: Supplier<Decimal>): SynchableDelegate<Decimal> {
		return add(delegate, StreamCodecs.DECIMAL)
	}

	fun item(value: ItemStack = ItemStack.EMPTY, setter: DelegateSetter<ItemStack> = DelegateSetter.passthrough(), getter: DelegateGetter<ItemStack> = DelegateGetter.passthrough()): SynchableDelegate<ItemStack> {
		return add(ListenableDelegate.maskSmart(value, getter, setter), ItemStackCodec)
	}

	fun computedItem(delegate: Supplier<ItemStack>): SynchableDelegate<ItemStack> {
		return computed(delegate, ItemStackCodec)
	}

	fun observedItem(value: ItemStack = ItemStack.EMPTY, setter: DelegateSetter<ItemStack> = DelegateSetter.passthrough(), getter: DelegateGetter<ItemStack> = DelegateGetter.passthrough()): SynchableObservedDelegate<ItemStack> {
		return add(Delegate.maskSmart(value, getter, setter), ItemStackCodec)
	}

	/**
	 * Remotes must be [close]d when no longer in use, otherwise they will
	 * leak memory and decrease performance of syncher.
	 *
	 * To get changes to be networked to remote client, [write] should be called.
	 */
	inner class Remote(private val listener: Runnable) : Closeable, IRemoteState {
		constructor() : this(Runnable {})

		private var isRemoved = false
		internal val dirty = ConcurrentLinkedQueue<RemoteSlot>()
		private val remoteSlots = ArrayList<RemoteSlot>()

		internal inner class RemoteSlot(val slot: Slot) {
			private val isDirty = AtomicBoolean()

			init {
				remoteSlots.add(this)
				dirty.add(this)
			}

			fun markDirty() {
				if (!isRemoved && isDirty.compareAndSet(false, true)) {
					dirty.add(this)
					listener.run()
				}
			}

			private val state: IRemoteState
			private val cleanable: Cleanable

			init {
				val weakSelf = WeakReference(this)
				val state = slot.synchable.createRemoteState(Runnable {
					weakSelf.get()?.markDirty()
				})

				// !!!
				// hOLY SHIT
				val weakState = WeakReference(state)
				cleanable = OTM_CLEANER.register(this) { weakState.get()?.close() }
				this.state = state
			}

			fun markClean() {
				isDirty.set(false)
			}

			fun invalidate() {
				state.invalidate()
				markDirty()
			}

			fun write(stream: RegistryFriendlyByteBuf) {
				state.write(stream)
			}

			fun remove() {
				isDirty.set(true)
				remoteSlots.remove(this)
				dirty.remove(this)
				cleanable.clean()
				slot.remoteSlots.remove(this)
			}

			override fun hashCode(): Int {
				return slot.hashCode()
			}

			override fun equals(other: Any?): Boolean {
				return this === other || other is RemoteSlot && other.slot == slot
			}
		}

		init {
			slots.forEach {
				if (it != null) {
					addSlot(it)
				}
			}

			remotes.add(this)
		}

		internal fun addSlot(slot: Slot) {
			slot.remoteSlots.add(RemoteSlot(slot))
		}

		/**
		 * Returns null if this remote is clean.
		 *
		 * [SynchableGroup.observe] is not called automatically for performance
		 * reasons, you must call it manually.
		 */
		fun write(registry: RegistryAccess): ByteArrayList? {
			if (dirty.isNotEmpty()) {
				return encodePayload(registry) {
					write(it)
				}
			}

			return null
		}

		/**
		 * Marks all networked slots dirty
		 */
		override fun invalidate() {
			remoteSlots.forEach { it.invalidate() }
		}

		override fun write(stream: RegistryFriendlyByteBuf) {
			val seen = ReferenceOpenHashSet<RemoteSlot>()
			val sorted = ArrayList<RemoteSlot>()
			var next = dirty.poll()

			while (next != null) {
				val status = seen.add(next)
				next.markClean()

				if (status) {
					sorted.add(next)
				}

				next = dirty.poll()
			}

			for (slot in sorted) {
				stream.writeVarInt(slot.slot.id + 1)
				slot.write(stream)
			}

			stream.writeByte(0)
		}

		override fun close() {
			if (!isRemoved) {
				isRemoved = true

				remoteSlots.toTypedArray().forEach {
					it.remove()
					it.slot.remoteSlots.remove(it)
				}

				remotes.remove(this)
				dirty.clear()
			}
		}
	}

	companion object {
		private val LOGGER = LogManager.getLogger()
	}
}
