
@file:Suppress("unused", "UNUSED_PARAMETER")

package ru.dbotthepony.mc.otm

import net.minecraft.client.server.IntegratedServer
import net.minecraft.server.MinecraftServer
import net.minecraft.world.level.Level
import net.minecraftforge.api.distmarker.Dist
import net.minecraftforge.event.TickEvent
import net.minecraftforge.event.TickEvent.ServerTickEvent
import net.minecraftforge.event.TickEvent.LevelTickEvent
import net.minecraftforge.event.server.ServerAboutToStartEvent
import net.minecraftforge.event.server.ServerStoppedEvent
import net.minecraftforge.event.server.ServerStoppingEvent
import net.minecraftforge.fml.loading.FMLLoader
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.block.entity.MatteryBlockEntity
import ru.dbotthepony.mc.otm.capability.AbstractProfiledStorage
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.core.collect.WeakHashSet
import ru.dbotthepony.mc.otm.core.util.AtomicallyInvalidatedLazy
import ru.dbotthepony.mc.otm.core.util.IConditionalTickable
import ru.dbotthepony.mc.otm.core.util.ITickable
import ru.dbotthepony.mc.otm.core.util.TickList
import ru.dbotthepony.mc.otm.graph.GraphNodeList
import ru.dbotthepony.mc.otm.network.MatteryNetworkChannel
import java.util.*
import java.util.concurrent.atomic.AtomicInteger

private val preServerTick = TickList()
private val postServerTick = TickList()
private val preWorldTick = WeakHashMap<Level, TickList>()
private val postWorldTick = WeakHashMap<Level, TickList>()

private val clientThreads = WeakHashSet<Thread>()
private val serverThreads = WeakHashSet<Thread>()

private val serverCounter = AtomicInteger()
private var _server: MinecraftServer? = null
val isClient: Boolean by lazy { FMLLoader.getDist() == Dist.CLIENT }

val UNIVERSE_TICKS get() = postServerTick.ticks
val Level.ticksPassed get() = postWorldTick.computeIfAbsent(this) { TickList() }.ticks

fun <V> lazyPerServer(fn: (MinecraftServer) -> V): Lazy<V> {
	return AtomicallyInvalidatedLazy(serverCounter) {
		if (!SERVER_IS_LIVE)
			throw IllegalStateException("No server is running")

		fn.invoke(_server!!)
	}
}

fun onceServerPre(inTicks: Int, callback: Runnable): TickList.Timer? {
	if (!SERVER_IS_LIVE) {
		LOGGER.error("Refusing to add timer $callback in ticks $inTicks while server is dying", IllegalStateException("Server is stopping"))
		return null
	}

	return preServerTick.Timer(inTicks, callback)
}

fun onceServer(inTicks: Int, callback: Runnable): TickList.Timer? {
	if (!SERVER_IS_LIVE) {
		LOGGER.error("Refusing to add ticker $callback in ticks $inTicks while server is dying", IllegalStateException("Server is stopping"))
		return null
	}

	return postServerTick.Timer(inTicks, callback)
}

/**
 * schedules execution of Runnable somewhere in the future,
 * at discretion of tick list
 */
fun sometimeServer(callback: Runnable): TickList.Timer? {
	if (!SERVER_IS_LIVE) {
		LOGGER.error("Refusing to add ticker $callback while server is dying", IllegalStateException("Server is stopping"))
		return null
	}

	return postServerTick.sometime(callback)
}

private val isPausedImpl: Boolean get() {
	val server = _server

	if (server is IntegratedServer) {
		return !server.isPublished && minecraft.isPaused
	}

	return false
}

val isPaused: Boolean get() {
	if (clientThreads.isEmpty()) {
		return false
	}

	return isPausedImpl
}

fun recordClientThread() {
	clientThreads.add(Thread.currentThread())
}

fun runIfClient(lambda: () -> Unit) {
	try {
		if (isClient) {
			lambda.invoke()
		}
	} catch(err: RuntimeException) {
		if (err.message?.startsWith("Attempted to load class") != true) {
			throw err
		}

		LOGGER.error("Unexpected classloading exception", err)
	}
}

fun <V> runIfClient(value: V, lambda: () -> V): V {
	try {
		if (isClient) {
			return lambda.invoke()
		}

		return value
	} catch(err: RuntimeException) {
		if (err.message?.startsWith("Attempted to load class") == true) {
			LOGGER.error("Unexpected classloading exception", err)
			return value
		}

		throw err
	}
}

fun <V> runOnClient(value: V, lambda: () -> V): V {
	if (isClientThread()) {
		return lambda.invoke()
	}

	return value
}

fun isServerThread(): Boolean {
	return Thread.currentThread() in serverThreads
}

fun isClientThread(): Boolean {
	return Thread.currentThread() in clientThreads
}

val MINECRAFT_SERVER: MinecraftServer
	get() {
		return _server ?: throw NullPointerException("Not running a server!")
	}

val NULLABLE_MINECRAFT_SERVER: MinecraftServer?
	get() {
		return _server
	}

// Flag whenever is server alive
// To avoid baby deadlocks caused by minecraft engine design issues
var SERVER_IS_LIVE = false
	private set

private val LOGGER = LogManager.getLogger()

fun onServerTick(event: ServerTickEvent) {
	if (event.phase === TickEvent.Phase.START) {
		preServerTick.tick()
		serverThreads.add(Thread.currentThread())
	} else {
		postServerTick.tick()
		// чтоб не плодить кучу подписчиков, вызовем напрямую отсюда
		GraphNodeList.tick()
		AbstractProfiledStorage.onServerPostTick()
		MatteryNetworkChannel.onServerPostTick()
	}
}

fun onLevelTick(event: LevelTickEvent) {
	if (event.phase === TickEvent.Phase.START) {
		preWorldTick[event.level]?.tick()

		if (event.side.isClient) {
			clientThreads.add(Thread.currentThread())
		} else if (event.side.isServer) {
			serverThreads.add(Thread.currentThread())
		}
	} else {
		postWorldTick[event.level]?.tick()
		MatteryBlockEntity.postLevelTick(event)
	}
}

fun onceServerPre(ticker: ITickable) {
	preServerTick.once(ticker, SERVER_IS_LIVE, "Server is stopping")
}

fun onceServer(ticker: ITickable) {
	postServerTick.once(ticker, SERVER_IS_LIVE, "Server is stopping")
}

fun tickServerPre(ticker: IConditionalTickable) {
	preServerTick.add(ticker, SERVER_IS_LIVE, "Server is stopping")
}

fun tickServer(ticker: IConditionalTickable) {
	postServerTick.add(ticker, SERVER_IS_LIVE, "Server is stopping")
}

fun Level.once(ticker: ITickable) {
	if (this.isClientSide) return

	if (!SERVER_IS_LIVE) {
		LOGGER.error("Refusing to add ticker $ticker while server is dying", IllegalStateException("Server is stopping"))
		return
	}

	postWorldTick.computeIfAbsent(this) { TickList() }.once(ticker)
}

fun Level.oncePre(ticker: ITickable) {
	if (this.isClientSide) return

	if (!SERVER_IS_LIVE) {
		LOGGER.error("Refusing to add ticker $ticker while server is dying", IllegalStateException("Server is stopping"))
		return
	}

	preWorldTick.computeIfAbsent(this) { TickList() }.once(ticker)
}

fun Level.addTicker(ticker: IConditionalTickable) {
	if (this.isClientSide) return

	if (!SERVER_IS_LIVE) {
		LOGGER.error("Refusing to add ticker $ticker while server is dying", IllegalStateException("Server is stopping"))
		return
	}

	postWorldTick.computeIfAbsent(this) { TickList() }.add(ticker)
}

fun Level.addTickerPre(ticker: IConditionalTickable) {
	if (this.isClientSide) return

	if (!SERVER_IS_LIVE) {
		LOGGER.error("Refusing to add ticker $ticker while server is dying", IllegalStateException("Server is stopping"))
		return
	}

	preWorldTick.computeIfAbsent(this) { TickList() }.add(ticker)
}

fun Level.until(ticker: () -> Boolean) {
	if (this.isClientSide) return
	addTicker(IConditionalTickable.wrap(ticker))
}

fun Level.untilPre(ticker: () -> Boolean) {
	if (this.isClientSide) return
	addTickerPre(IConditionalTickable.wrap(ticker))
}

fun Level.`while`(condition: () -> Boolean, ticker: () -> Unit) {
	if (this.isClientSide) return
	addTicker(IConditionalTickable.wrap(condition, ticker))
}

fun Level.whilePre(condition: () -> Boolean, ticker: () -> Unit) {
	if (this.isClientSide) return
	addTickerPre(IConditionalTickable.wrap(condition, ticker))
}

private fun clear() {
	preServerTick.clear()
	postServerTick.clear()
	preWorldTick.clear()
	postWorldTick.clear()
}

fun onServerStarting(event: ServerAboutToStartEvent) {
	clear()
	SERVER_IS_LIVE = true
	_server = event.server
	serverThreads.add(Thread.currentThread())
	serverCounter.incrementAndGet()
	MatteryNetworkChannel.onServerStarting()
}

fun onServerStopping(event: ServerStoppingEvent) {
	clear()
	SERVER_IS_LIVE = false
	serverCounter.incrementAndGet()
	MatteryNetworkChannel.onServerStopping()
}

fun onServerStopped(event: ServerStoppedEvent) {
	if (SERVER_IS_LIVE) {
		LOGGER.fatal("ServerStoppingEvent did not fire. If server has crashed this is normal. However, if server finished it's work 'gracefully' this is a bug!")

		clear()
		SERVER_IS_LIVE = false
	}

	_server = null
	serverCounter.incrementAndGet()
	MatteryNetworkChannel.onServerStopped()
}
