package ru.dbotthepony.mc.otm.network

import net.minecraft.network.FriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.player.Player
import net.minecraftforge.network.NetworkDirection
import net.minecraftforge.network.NetworkEvent
import net.minecraftforge.network.NetworkRegistry
import net.minecraftforge.network.PacketDistributor
import net.minecraftforge.network.simple.SimpleChannel
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.NULLABLE_MINECRAFT_SERVER
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.core.GetterSetter
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.locks.LockSupport
import java.util.function.BiConsumer
import java.util.function.Function
import java.util.function.Predicate
import java.util.function.Supplier
import kotlin.reflect.KClass

class MNetworkContext(val sender: ServerPlayer?, packetHandled: GetterSetter<Boolean>, private val enqueuer: (Runnable) -> CompletableFuture<*>) {
	var packetHandled by packetHandled

	fun enqueueWork(callback: Runnable): CompletableFuture<*> {
		return enqueuer(callback)
	}
}

interface MatteryPacket {
	fun write(buff: FriendlyByteBuf)
	fun play(context: MNetworkContext)
}

abstract class MatteryNetworkChannel(val version: Int, val name: String) {
	val channel: SimpleChannel = NetworkRegistry
		.newSimpleChannel(ResourceLocation(OverdriveThatMatters.MOD_ID, name), { version.toString() }, { it == version.toString() }, { it == version.toString() })

	fun sendToServer(packet: Any) = channel.send(PacketDistributor.SERVER.noArg(), packet)

	fun send(ply: Player, packet: Any) {
		if (ply is ServerPlayer) {
			queue.add(Task(channel, PacketDistributor.PLAYER.with { ply }, packet))
		}
	}

	fun sendNow(ply: Player, packet: Any) {
		if (ply is ServerPlayer) {
			channel.send(PacketDistributor.PLAYER.with { ply }, packet)
		}
	}

	fun sendTracking(entity: Entity, packet: Any) {
		if ((NULLABLE_MINECRAFT_SERVER?.playerCount ?: 0) <= 0) {
			return
		}

		queue.add(Task(channel, PacketDistributor.TRACKING_ENTITY.with { entity }, packet))
	}

	fun sendTrackingAndSelf(entity: Entity, packet: Any) {
		if ((NULLABLE_MINECRAFT_SERVER?.playerCount ?: 0) <= 0) {
			return
		}

		queue.add(Task(channel, PacketDistributor.TRACKING_ENTITY_AND_SELF.with { entity }, packet))
	}

	fun send(distributor: PacketDistributor.PacketTarget, packet: Any) {
		queue.add(Task(channel, distributor, packet))
	}

	fun sendNow(distributor: PacketDistributor.PacketTarget, packet: Any) {
		channel.send(distributor, packet)
	}

	private var nextNetworkPacketID = 0

	fun <T> add(
		packetClass: Class<T>,
		writer: BiConsumer<T, FriendlyByteBuf>,
		reader: Function<FriendlyByteBuf, T>,
		handler: (T, MNetworkContext) -> Unit,
		direction: NetworkDirection? = null,
		handleOnMainThread: Boolean = true,
	) {
		if (nextNetworkPacketID >= 256) {
			throw IndexOutOfBoundsException("Network message ID overflow!")
		}

		val builder = channel.messageBuilder(packetClass, nextNetworkPacketID++)
		val bridgeHandler = BiConsumer<T, Supplier<NetworkEvent.Context>> { a, b -> handler(a, MNetworkContext(b.get().sender, GetterSetter.of({ b.get().packetHandled }, { b.get().packetHandled = it }), b.get()::enqueueWork)) }

		if (handleOnMainThread) {
			builder.consumerMainThread(bridgeHandler)
		} else {
			builder.consumerNetworkThread(bridgeHandler)
		}

		builder.encoder(writer)
		builder.decoder(reader)
		builder.add()
	}

	fun <T : MatteryPacket> add(
		packetClass: Class<T>,
		reader: Function<FriendlyByteBuf, T>,
		direction: NetworkDirection? = null,
		handleOnMainThread: Boolean = true,
	) {
		add(packetClass, MatteryPacket::write, reader, MatteryPacket::play, direction, handleOnMainThread)
	}

	fun <T : MatteryPacket> add(
		packetClass: KClass<T>,
		reader: Function<FriendlyByteBuf, T>,
		direction: NetworkDirection? = null,
		handleOnMainThread: Boolean = true,
	) {
		add(packetClass.java, MatteryPacket::write, reader, MatteryPacket::play, direction, handleOnMainThread)
	}

	private data class Task(val channel: SimpleChannel, val target: PacketDistributor.PacketTarget, val packet: Any)

	companion object {
		private val logger = LogManager.getLogger()
		private var thread: Thread? = null

		private val queue = ConcurrentLinkedQueue<Task>()

		@Volatile
		private var interrupt = false

		/**
		 * Reason behind separate thread is that Minecraft connection class does not allow to enqueue messages,
		 * nor does Forge. This leads to extensive calls to Netty Channel eventLoop().execute(), which is somehow resource
		 * heavy, as it calls wakeup on Windows, eventFdWrite on Linux, etc., when all we want to do is to enqueue packet.
		 *
		 * Concurrent safety - Minecraft Channel (send method) does not do any writes (unless player is unconnected, in which case it uses thread safe queue), hence it is thread safe.
		 * OTM packets are always dispatched in correct order to client.
		 * Netty eventLoop executor is thread safe by definition.
		 */
		private fun run() {
			while (!interrupt) {
				val task = queue.poll()

				if (task == null) {
					LockSupport.park()
				} else {
					try {
						task.channel.send(task.target, task.packet)
					} catch(err: Throwable) {
						logger.error("Error executing network dispatcher task", err)
					}
				}
			}

			logger.debug("Overdrive That Matters Packet Dispatcher thread exited gracefully")
		}

		internal fun onServerPostTick() {
			if (queue.isNotEmpty()) {
				LockSupport.unpark(thread)
			}
		}

		internal fun onServerStarting() {
			interrupt = false
			check(thread?.isAlive != true) { "Already having network dispatcher thread, ServerStartingEvent was fired twice!" }
			thread = Thread(this::run, "Overdrive That Matters Network Dispatcher").also { it.isDaemon = true; it.start() }
		}

		internal fun onServerStopping() {
			interrupt = true
			LockSupport.unpark(thread)
		}

		internal fun onServerStopped() {
			interrupt = true
			LockSupport.unpark(thread)
		}
	}
}
