package ru.dbotthepony.kstarbound

import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Interner
import com.github.benmanes.caffeine.cache.Scheduler
import com.google.gson.*
import com.google.gson.stream.JsonReader
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.asCoroutineDispatcher
import org.apache.logging.log4j.LogManager
import org.classdump.luna.compiler.CompilerChunkLoader
import org.classdump.luna.compiler.CompilerSettings
import org.classdump.luna.load.ChunkFactory
import ru.dbotthepony.kommons.gson.AABBTypeAdapter
import ru.dbotthepony.kommons.gson.AABBiTypeAdapter
import ru.dbotthepony.kommons.gson.EitherTypeAdapter
import ru.dbotthepony.kommons.gson.KOptionalTypeAdapter
import ru.dbotthepony.kommons.gson.NothingAdapter
import ru.dbotthepony.kommons.gson.Vector2dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector2fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector2iTypeAdapter
import ru.dbotthepony.kommons.gson.Vector3dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector3fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector3iTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.*
import ru.dbotthepony.kstarbound.defs.image.Image
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList
import ru.dbotthepony.kstarbound.defs.animation.Particle
import ru.dbotthepony.kstarbound.defs.quest.QuestParameter
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParametersType
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables
import ru.dbotthepony.kstarbound.defs.world.BiomePlacementDistributionType
import ru.dbotthepony.kstarbound.defs.world.BiomePlacementItemType
import ru.dbotthepony.kstarbound.defs.world.WorldLayout
import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType
import ru.dbotthepony.kstarbound.io.*
import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.json.factory.MapsTypeAdapterFactory
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.json.InternedStringAdapter
import ru.dbotthepony.kstarbound.json.LongRangeAdapter
import ru.dbotthepony.kstarbound.json.builder.EnumAdapter
import ru.dbotthepony.kstarbound.json.builder.BuilderAdapter
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.json.JsonImplementationTypeFactory
import ru.dbotthepony.kstarbound.json.factory.CollectionAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.ImmutableCollectionAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.PairAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.RGBAColorTypeAdapter
import ru.dbotthepony.kstarbound.json.factory.SingletonTypeAdapterFactory
import ru.dbotthepony.kstarbound.server.world.UniverseChunk
import ru.dbotthepony.kstarbound.item.RecipeRegistry
import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory
import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.ExecutorWithScheduler
import ru.dbotthepony.kstarbound.util.Directives
import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.*
import java.lang.ref.Cleaner
import java.text.DateFormat
import java.time.Duration
import java.util.Collections
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadFactory
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport
import kotlin.NoSuchElementException
import kotlin.collections.ArrayList

object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLocator {
	const val ENGINE_VERSION = "0.0.1"
	const val NATIVE_PROTOCOL_VERSION = 748
	const val LEGACY_PROTOCOL_VERSION = 747
	const val TIMESTEP = 1.0 / 60.0
	const val SYSTEM_WORLD_TIMESTEP = 1.0 / 20.0
	const val TIMESTEP_NANOS = (TIMESTEP * 1_000_000_000L).toLong()
	const val SYSTEM_WORLD_TIMESTEP_NANOS = (SYSTEM_WORLD_TIMESTEP * 1_000_000_000L).toLong()

	// compile flags. uuuugh
	const val DEDUP_CELL_STATES = true
	const val USE_CAFFEINE_INTERNER = false
	const val USE_INTERNER = true

	fun <E : Any> interner(): Interner<E> {
		if (!USE_INTERNER)
			return Interner { it }

		return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner()
	}

	fun <E : Any> interner(bits: Int): Interner<E> {
		if (!USE_INTERNER)
			return Interner { it }

		return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner(bits)
	}

	private val LOGGER = LogManager.getLogger()

	override fun schedule(executor: Executor, command: Runnable, delay: Long, unit: TimeUnit): Future<*> {
		return schedule(Runnable { executor.execute(command) }, delay, unit)
	}

	init {
		isDaemon = true
		start()
	}

	private val ioPoolCounter = AtomicInteger()

	@JvmField
	val IO_EXECUTOR: ExecutorService = ThreadPoolExecutor(0, 64, 30L, TimeUnit.SECONDS, LinkedBlockingQueue(), ThreadFactory {
		val thread = Thread(it, "Starbound Storage IO ${ioPoolCounter.getAndIncrement()}")
		thread.isDaemon = true
		thread.priority = Thread.MIN_PRIORITY

		thread.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e ->
			LOGGER.error("I/O thread died due to uncaught exception", e)
		}

		return@ThreadFactory thread
	})

	@JvmField
	val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool()
	@JvmField
	val COROUTINE_EXECUTOR = ExecutorWithScheduler(EXECUTOR, this).asCoroutineDispatcher()

	@JvmField
	val CLEANER: Cleaner = Cleaner.create {
		val t = Thread(it, "Starbound Global Cleaner")
		t.isDaemon = true
		t.priority = 2
		t
	}

	// currently Caffeine one saves only 4 megabytes of RAM on pretty big modpack
	// Hrm.
	// val strings: Interner<String> = Interner.newWeakInterner()
	// val strings: Interner<String> = Interner { it }
	@JvmField
	val STRINGS: Interner<String> = interner(5)

	// immeasurably lazy and fragile solution, too bad!
	// While having four separate Gson instances look like a (much) better solution (and it indeed could have been!),
	// we must not forget the fact that 'Starbound' and 'Consistent data format' are opposites,
	// and there are cases of where discStore() calls toJson() on children data, despite it having its own discStore() too.
	var IS_LEGACY_JSON: Boolean by ThreadLocal.withInitial { false }
		private set
	var IS_STORE_JSON: Boolean by ThreadLocal.withInitial { false }
		private set

	fun legacyJson(data: Any): JsonElement {
		try {
			IS_LEGACY_JSON = true
			return gson.toJsonTree(data)
		} finally {
			IS_LEGACY_JSON = false
		}
	}

	fun storeJson(data: Any): JsonElement {
		try {
			IS_STORE_JSON = true
			return gson.toJsonTree(data)
		} finally {
			IS_STORE_JSON = false
		}
	}

	fun legacyStoreJson(data: Any): JsonElement {
		try {
			IS_STORE_JSON = true
			IS_LEGACY_JSON = true
			return gson.toJsonTree(data)
		} finally {
			IS_STORE_JSON = false
			IS_LEGACY_JSON = false
		}
	}

	fun <T> legacyJson(block: () -> T): T {
		try {
			IS_LEGACY_JSON = true
			return block.invoke()
		} finally {
			IS_LEGACY_JSON = false
		}
	}

	fun <T> storeJson(block: () -> T): T {
		try {
			IS_STORE_JSON = true
			return block.invoke()
		} finally {
			IS_STORE_JSON = false
		}
	}

	fun <T> legacyStoreJson(block: () -> T): T {
		try {
			IS_STORE_JSON = true
			IS_LEGACY_JSON = true
			return block.invoke()
		} finally {
			IS_STORE_JSON = false
			IS_LEGACY_JSON = false
		}
	}

	private val loader = CompilerChunkLoader.of(CompilerSettings.defaultNoAccountingSettings(), "sb_lua_")
	private val scriptCache = ConcurrentHashMap<String, ChunkFactory>()

	private fun loadScript0(path: String): ChunkFactory {
		val find = locate(path)

		if (!find.exists) {
			throw NoSuchElementException("Script $path does not exist")
		}

		val time = System.nanoTime()
		val result = loader.compileTextChunk(path, find.readToString())
		LOGGER.debug("Compiled {} in {} ms", path, (System.nanoTime() - time) / 1_000_000L)
		return result
	}

	fun loadScript(path: String): ChunkFactory {
		return scriptCache.computeIfAbsent(path, ::loadScript0)
	}

	fun compileScriptChunk(name: String, chunk: String): ChunkFactory {
		return loader.compileTextChunk(name, chunk)
	}

	val ELEMENTS_ADAPTER = InternedJsonElementAdapter(STRINGS)

	val gson: Gson = with(GsonBuilder()) {
		// serializeNulls()
		setDateFormat(DateFormat.LONG)
		setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
		setPrettyPrinting()

		registerTypeAdapter(InternedStringAdapter(STRINGS))

		registerTypeAdapter(ELEMENTS_ADAPTER)
		registerTypeAdapter(ELEMENTS_ADAPTER.arrays)
		registerTypeAdapter(ELEMENTS_ADAPTER.objects)

		registerTypeAdapter(Nothing::class.java, NothingAdapter)

		// Обработчик @JsonImplementation
		registerTypeAdapterFactory(JsonImplementationTypeFactory)
		registerTypeAdapterFactory(JsonAdapterTypeFactory)

		// списки, наборы, т.п.
		registerTypeAdapterFactory(CollectionAdapterFactory)

		// ImmutableList, ImmutableSet, ImmutableMap
		registerTypeAdapterFactory(ImmutableCollectionAdapterFactory(STRINGS))

		// fastutil collections
		registerTypeAdapterFactory(MapsTypeAdapterFactory(STRINGS))

		// все enum'ы без особых настроек
		registerTypeAdapterFactory(EnumAdapter.Companion)

		// @JsonBuilder
		registerTypeAdapterFactory(BuilderAdapter.Factory(STRINGS))

		// @JsonFactory
		registerTypeAdapterFactory(FactoryAdapter.Factory(STRINGS))

		// Either<>
		registerTypeAdapterFactory(EitherTypeAdapter)
		// KOptional<>
		registerTypeAdapterFactory(KOptionalTypeAdapter)

		registerTypeAdapterFactory(SingletonTypeAdapterFactory)

		// Pair<>
		registerTypeAdapterFactory(PairAdapterFactory)
		registerTypeAdapterFactory(SBPattern.Companion)

		registerTypeAdapterFactory(JsonReference.Companion)

		registerTypeAdapter(ColorReplacements.Companion)
		registerTypeAdapterFactory(BlueprintLearnList.Companion)

		registerTypeAdapter(RGBAColorTypeAdapter)

		registerTypeAdapterFactory(NativeLegacy.Companion)

		// математические классы
		registerTypeAdapter(AABBTypeAdapter.nullSafe())
		registerTypeAdapter(AABBiTypeAdapter.nullSafe())
		registerTypeAdapter(Vector2dTypeAdapter.nullSafe())
		registerTypeAdapter(Vector2fTypeAdapter.nullSafe())
		registerTypeAdapter(Vector2iTypeAdapter.nullSafe())
		registerTypeAdapter(Vector3dTypeAdapter.nullSafe())
		registerTypeAdapter(Vector3fTypeAdapter.nullSafe())
		registerTypeAdapter(Vector3iTypeAdapter.nullSafe())
		registerTypeAdapter(Vector4iTypeAdapter.nullSafe())
		registerTypeAdapter(Vector4dTypeAdapter.nullSafe())
		registerTypeAdapter(Vector4fTypeAdapter.nullSafe())
		registerTypeAdapterFactory(AbstractPerlinNoise.Companion)
		registerTypeAdapterFactory(WeightedList.Companion)

		// Функции
		registerTypeAdapter(JsonFunction.CONSTRAINT_ADAPTER)
		registerTypeAdapter(JsonFunction.INTERPOLATION_ADAPTER)
		registerTypeAdapter(JsonFunction.Companion)
		registerTypeAdapter(JsonConfigFunction::Adapter)
		registerTypeAdapterFactory(Json2Function.Companion)

		// Общее
		registerTypeAdapterFactory(ThingDescription.Factory(STRINGS))
		registerTypeAdapterFactory(TerrainSelectorType.Companion)

		registerTypeAdapter(Directives.Companion)

		registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.DAMAGE))

		registerTypeAdapterFactory(AssetPath.Companion)
		registerTypeAdapter(SpriteReference.Companion)

		registerTypeAdapterFactory(AssetReference.Companion)

		registerTypeAdapterFactory(UniverseChunk.Companion)

		registerTypeAdapter(Image.Companion)

		registerTypeAdapterFactory(Poly.Companion)

		registerTypeAdapter(CelestialParameters::Adapter)
		registerTypeAdapter(Particle::Adapter)
		registerTypeAdapter(QuestParameter::Adapter)

		registerTypeAdapterFactory(BiomePlacementDistributionType.DEFINITION_ADAPTER)
		registerTypeAdapterFactory(BiomePlacementItemType.DATA_ADAPTER)
		registerTypeAdapterFactory(BiomePlacementItemType.DEFINITION_ADAPTER)
		registerTypeAdapterFactory(BiomePlaceables.Item.Companion)

		// register companion first, so it has lesser priority than dispatching adapter
		registerTypeAdapterFactory(VisitableWorldParametersType.Companion)
		registerTypeAdapterFactory(VisitableWorldParametersType.ADAPTER)
		registerTypeAdapter(WorldLayout.Companion)

		Registries.registerAdapters(this)

		registerTypeAdapter(LongRangeAdapter)

		create()
	}

	var initializing = false
		private set
	var initialized = false
		private set
	var bootstrapping = false
		private set
	var bootstrapped = false
		private set
	var loadingProgress = 0.0
		private set
	var toLoad = 0
		private set
	var loaded = 0
		private set

	private val jsonAssetsCache = Caffeine.newBuilder()
		.maximumSize(4096L)
		.expireAfterAccess(Duration.ofMinutes(5L))
		.scheduler(this)
		.executor(EXECUTOR)
		.build<String, KOptional<JsonElement>>()

	fun loadJsonAsset(path: String): JsonElement? {
		val filename: String
		val jsonPath: String?

		if (path.contains(':')) {
			filename = path.substringBefore(':')
			jsonPath = path.substringAfter(':')
		} else {
			filename = path
			jsonPath = null
		}

		val json = jsonAssetsCache.get(filename) {
			val file = locate(it)

			if (!file.isFile)
				return@get KOptional()

			val findPatches = locateAll("$filename.patch")
			KOptional(JsonPatch.apply(ELEMENTS_ADAPTER.read(file.jsonReader()), findPatches))
		}.orNull() ?: return null

		if (jsonPath == null)
			return json

		return JsonPath.query(jsonPath).get(json)
	}

	private val fileSystems = ArrayList<IStarboundFile>()
	private val toLoadPaks = ObjectArraySet<File>()
	private val toLoadPaths = ObjectArraySet<File>()

	var loadingProgressText: String = ""
		private set

	fun addArchive(path: File) {
		toLoadPaks.add(path)
	}

	fun addPath(path: File) {
		toLoadPaths.add(path)
	}

	fun doBootstrap() {
		if (!bootstrapped && !bootstrapping) {
			bootstrapping = true
		} else {
			return
		}

		val fileSystems = ArrayList<Pair<Long, IStarboundFile>>()

		for (path in toLoadPaks) {
			LOGGER.info("Reading PAK archive $path")

			try {
				loadingProgressText = "Indexing $path"
				val pak = StarboundPak(path) { _, s -> loadingProgressText = "Indexing $path: $s" }
				val priority = pak.metadata.get("priority", 0L)
				fileSystems.add(priority to pak.root)
			} catch (err: Throwable) {
				LOGGER.error("Error reading PAK archive $path. Not a PAK archive?")
			}
		}

		for (path in toLoadPaths) {
			val metadata: JsonObject
			val metadataPath = File(path, "_metadata")

			if (metadataPath.exists() && metadataPath.isFile) {
				metadata = gson.fromJson(JsonReader(metadataPath.reader()), JsonObject::class.java)
			} else {
				metadata = JsonObject()
			}

			val priority = metadata.get("priority", 0L)
			fileSystems.add(priority to PhysicalFile(path))
		}

		fileSystems.sortByDescending { it.first }

		for ((_, fs) in fileSystems) {
			this.fileSystems.add(fs)
		}

		LOGGER.info("Finished reading PAK archives")
		bootstrapped = true
		bootstrapping = false
	}

	fun bootstrapGame(): CompletableFuture<*> {
		return submit { doBootstrap() }
	}

	override fun exists(path: String): Boolean {
		@Suppress("name_shadowing")
		var path = path

		if (path[0] == '/') {
			path = path.substring(1)
		}

		for (fs in fileSystems) {
			if (fs.locate(path).exists) {
				return true
			}
		}

		return false
	}

	override fun locate(path: String): IStarboundFile {
		@Suppress("name_shadowing")
		var path = path

		if (path[0] == '/') {
			path = path.substring(1)
		}

		for (fs in fileSystems) {
			val file = fs.locate(path)

			if (file.exists) {
				return file
			}
		}

		return NonExistingFile(path.split("/").last(), fullPath = path)
	}

	fun locateAll(path: String): List<IStarboundFile> {
		@Suppress("name_shadowing")
		var path = path

		if (path[0] == '/') {
			path = path.substring(1)
		}

		val files = ArrayList<IStarboundFile>()

		for (fs in fileSystems.asReversed()) {
			val file = fs.locate(path)

			if (file.exists) {
				files.add(file)
			}
		}

		return files
	}

	private fun doInitialize() {
		if (!initializing && !initialized) {
			initializing = true
		} else {
			return
		}

		doBootstrap()

		loadingProgressText = "Building file tree..."
		val fileTree = HashMap<String, HashSet<IStarboundFile>>()
		val patchTree = HashMap<String, ArrayList<IStarboundFile>>()

		// finding assets, assets originating from top-most priority PAKs are overriding
		// same assets from other PAKs
		fileSystems.forEach {
			it.explore { file ->
				if (file.isFile)
					fileTree.computeIfAbsent(file.name.substringAfterLast('.')) { HashSet() }.add(file)
			}
		}

		// finding asset patches, patches from bottom-most priority PAKs are applied first
		fileSystems.asReversed().forEach {
			it.explore { file ->
				if (file.isFile && file.name.endsWith(".patch"))
					patchTree.computeIfAbsent(file.computeFullPath().substringAfterLast('.')) { ArrayList() }.add(file)
			}
		}

		loadingProgressText = "Dispatching load tasks..."
		val tasks = ArrayList<Future<*>>()

		tasks.addAll(Registries.load(fileTree, patchTree))
		tasks.addAll(RecipeRegistry.load(fileTree, patchTree))
		tasks.addAll(Globals.load())
		tasks.add(VersionRegistry.load(patchTree))

		val total = tasks.size.toDouble()
		toLoad = tasks.size

		while (tasks.isNotEmpty()) {
			tasks.removeIf { it.isDone }
			loaded = toLoad - tasks.size
			loadingProgress = (total - tasks.size) / total
			loadingProgressText = "Loading JSON assets, $loaded / $toLoad"
			LockSupport.parkNanos(5_000_000L)
		}

		Registries.finishLoad()
		RecipeRegistry.finishLoad()
		ItemRegistry.finishLoad()

		Registries.validate()

		initializing = false
		initialized = true
	}

	fun initializeGame(): CompletableFuture<*> {
		return submit { doInitialize() }
	}

	private var fontPath: File? = null

	fun loadFont(): CompletableFuture<File> {
		val fontPath = fontPath

		if (fontPath != null)
			return CompletableFuture.completedFuture(fontPath)

		return supplyAsync {
			val fontPath = Starbound.fontPath

			if (fontPath != null)
				return@supplyAsync fontPath

			val file = locate("/hobo.ttf")

			if (!file.exists)
				throw FileNotFoundException("Unable to locate font file /hobo.ttf")
			else if (!file.isFile)
				throw FileNotFoundException("/hobo.ttf is not a file!")
			else {
				val tempPath = File(System.getProperty("java.io.tmpdir"), "sb-hobo.ttf")
				tempPath.writeBytes(file.read().array())
				Starbound.fontPath = tempPath
				return@supplyAsync tempPath
			}
		}
	}
}