
@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.capability.AbstractProfiledStorage
import ru.dbotthepony.mc.otm.capability.energy.ProfiledEnergyStorage
import ru.dbotthepony.mc.otm.client.minecraft
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.Abstract6Graph
import java.util.*

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

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)
}

private var _server: MinecraftServer? = null
private var _serverThread: Thread? = null
private var _clientThread: Thread? = null

val isClient: Boolean by lazy { FMLLoader.getDist() == Dist.CLIENT }

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

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

	return false
}

val isPaused: Boolean get() {
	if (_clientThread === null) {
		return false
	}

	return isPausedImpl
}

fun recordClientThread() {
	if (_clientThread != null) {
		throw IllegalStateException("Already have client channel")
	}

	_clientThread = 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() === _serverThread
}

fun isClientThread(): Boolean {
	return Thread.currentThread() === _clientThread
}

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()
	} else {
		postServerTick.tick()
		// чтоб не плодить кучу подписчиков, вызовем напрямую отсюда
		Abstract6Graph.tick()
		AbstractProfiledStorage.onServerPostTick()
	}
}

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

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 tickUntilServerPre(ticker: () -> Boolean) {
	preServerTick.until(ticker, SERVER_IS_LIVE, "Server is stopping")
}

fun tickUntilServer(ticker: () -> Boolean) {
	postServerTick.until(ticker, SERVER_IS_LIVE, "Server is stopping")
}

fun tickWhileServerPre(condition: () -> Boolean, ticker: () -> Unit) {
	preServerTick.`while`(condition, ticker, SERVER_IS_LIVE, "Server is stopping")
}

fun tickWhileServer(condition: () -> Boolean, ticker: () -> Unit) {
	postServerTick.`while`(condition, 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
	_serverThread = Thread.currentThread()
}

fun onServerStopping(event: ServerStoppingEvent) {
	clear()
	SERVER_IS_LIVE = false
}

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
	_serverThread = null
}
