package ru.dbotthepony.mc.otm.capability.drive

import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import java.util.UUID
import net.minecraft.ReportedException
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtIo
import net.minecraft.CrashReport
import net.minecraft.Util
import net.minecraft.world.level.storage.LevelResource
import java.util.concurrent.locks.LockSupport
import net.minecraftforge.event.server.ServerAboutToStartEvent
import net.minecraftforge.event.server.ServerStoppingEvent
import net.minecraftforge.event.level.LevelEvent
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.NULLABLE_MINECRAFT_SERVER
import ru.dbotthepony.mc.otm.SERVER_IS_LIVE
import ru.dbotthepony.mc.otm.isServerThread
import java.io.File
import java.lang.ref.WeakReference
import java.util.ArrayList
import java.util.concurrent.ConcurrentLinkedQueue

private class WeakDriveReference(drive: IMatteryDrive<*>) {
	private var drive: IMatteryDrive<*>? = drive
	private val weak = WeakReference(drive)

	val isValid: Boolean get() {
		return drive != null || weak.get() != null
	}

	fun drive(): IMatteryDrive<*>? {
		return drive ?: weak.get()
	}

	fun sync(): CompoundTag? {
		val drive = drive() ?: return null

		if (!drive.isDirty) {
			this.drive = null
			return null
		}

		val tag = drive.serializeNBT()
		drive.isDirty = false
		this.drive = null

		return tag
	}

	fun access(): IMatteryDrive<*>? {
		this.drive = weak.get()
		return drive
	}
}

private data class BacklogLine(val uuid: UUID, val tag: CompoundTag)

/**
 * Separate thread and separate files are way faster for enormous volumes of data (if mod is being run on public dedicated server)
 */
object DrivePool {
	private val resource = LevelResource("otm_drives")
	private val LOGGER = LogManager.getLogger()
	private val pool = Object2ObjectOpenHashMap<UUID, WeakDriveReference>()

	private val backlog = ConcurrentLinkedQueue<BacklogLine>()
	private var knownBaseDirectory: File? = null

	private val baseDirectory: File? get() {
		val server = NULLABLE_MINECRAFT_SERVER ?: return null

		val baseDirectory = server.storageSource.getLevelPath(resource).toFile()

		if (knownBaseDirectory != baseDirectory) {
			baseDirectory.mkdirs()
			knownBaseDirectory = baseDirectory
		}

		return baseDirectory
	}

	operator fun <T : IMatteryDrive<*>> get(id: UUID, deserializer: (CompoundTag) -> T, factory: () -> T): T {
		if (!isServerThread())
			throw IllegalAccessException("Can not access drive pool from outside of server thread.")

		if (!SERVER_IS_LIVE)
			throw IllegalStateException("Server is not running")

		val get = pool[id]?.access()

		if (get != null) {
			return get as T
		}

		val file = File(baseDirectory, "$id.dat")

		if (file.exists()) {
			try {
				val factorize = deserializer(NbtIo.readCompressed(file))
				pool[id] = WeakDriveReference(factorize)
				return factorize
			} catch (error: Throwable) {
				val report = CrashReport("Reading OTM Drive from disk", error)
				val category = report.addCategory("Corrupt drive")
				category.setDetail("UUID", id)
				category.setDetail("File location", file)
				throw ReportedException(report)
			}
		}

		return factory().also { pool[id] = WeakDriveReference(it) }
	}

	fun markDirty(id: UUID) {
		if (isServerThread()) {
			pool[id]?.access()
		}
	}

	fun onWorldSave(event: LevelEvent.Save) {
		writeBacklog()
	}

	private fun writeBacklog() {
		var needsSync = false
		val removeKeys = ArrayList<UUID>()

		for ((key, value) in pool) {
			try {
				val tag = value.sync()

				if (tag != null) {
					LOGGER.debug("Serializing OTM Drive {}", key)
					backlog.add(BacklogLine(key, tag))
					needsSync = true
				} else if (!value.isValid) {
					removeKeys.add(key)
				}
			} catch (err: Throwable) {
				LOGGER.error("Failed to serialize OTM Drive for persistent storage with ID $key", err)
			}
		}

		for (key in removeKeys) {
			pool.remove(key)
		}

		if (needsSync) {
			Util.backgroundExecutor().execute(::sync)
		}
	}

	private fun sync() {
		while (true) {
			val next = backlog.poll() ?: return
			val (uuid, tag) = next

			try {
				LOGGER.debug("Syncing OTM Drive {}", uuid)

				val oldFile = File(baseDirectory, "$uuid.dat_old")
				val newFile = File(baseDirectory, "$uuid.dat")

				if (oldFile.exists()) check(oldFile.delete()) { "Unable to delete old dat file" }
				if (newFile.exists()) check(newFile.renameTo(oldFile)) { "Unable to move old dat file" }

				NbtIo.writeCompressed(tag, newFile)
			} catch (error: Throwable) {
				LOGGER.error("Failed to sync OTM Drive to persistent storage with ID $uuid", error)
			}
		}
	}
}
