package ru.dbotthepony.kstarbound.item

import com.google.gson.JsonElement
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.IStarboundFile
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.actor.player.RecipeDefinition
import ru.dbotthepony.kstarbound.json.JsonPatch
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Future
import kotlin.collections.ArrayList

object RecipeRegistry {
	private val LOGGER = LogManager.getLogger()

	data class Entry(val value: RecipeDefinition, val json: JsonElement, val file: IStarboundFile)

	private val recipesInternal = ArrayList<Entry>()
	private val group2recipesInternal = Object2ObjectOpenHashMap<String, ArrayList<Entry>>()
	private val group2recipesBacking = Object2ObjectOpenHashMap<String, List<Entry>>()
	private val output2recipesInternal = Object2ObjectOpenHashMap<String, ArrayList<Entry>>()
	private val output2recipesBacking = Object2ObjectOpenHashMap<String, List<Entry>>()
	private val input2recipesInternal = Object2ObjectOpenHashMap<String, ArrayList<Entry>>()
	private val input2recipesBacking = Object2ObjectOpenHashMap<String, List<Entry>>()

	val recipes: List<Entry> = Collections.unmodifiableList(recipesInternal)
	val group2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(group2recipesBacking)
	val output2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(output2recipesBacking)
	val input2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(input2recipesBacking)

	private val tasks = ConcurrentLinkedQueue<Entry>()

	private fun add(recipe: Entry) {
		val value = recipe.value
		recipesInternal.add(recipe)

		for (group in value.groups) {
			group2recipesInternal.computeIfAbsent(group, Object2ObjectFunction { p ->
				ArrayList<Entry>(1).also {
					group2recipesBacking[p as String] = Collections.unmodifiableList(it)
				}
			}).add(recipe)
		}

		output2recipesInternal.computeIfAbsent(value.output.name, Object2ObjectFunction { p ->
			ArrayList<Entry>(1).also {
				output2recipesBacking[p as String] = Collections.unmodifiableList(it)
			}
		}).add(recipe)

		for (input in value.input) {
			input2recipesInternal.computeIfAbsent(input.name, Object2ObjectFunction { p ->
				ArrayList<Entry>(1).also {
					input2recipesBacking[p as String] = Collections.unmodifiableList(it)
				}
			}).add(recipe)
		}
	}

	fun finishLoad() {
		tasks.forEach { add(it) }
		tasks.clear()
	}

	fun load(fileTree: Map<String, Collection<IStarboundFile>>, patchTree: Map<String, List<IStarboundFile>>): List<Future<*>> {
		val files = fileTree["recipe"] ?: return emptyList()

		val recipes by lazy { Starbound.gson.getAdapter(RecipeDefinition::class.java) }

		return files.map { listedFile ->
			Starbound.EXECUTOR.submit {
				try {
					val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patchTree[listedFile.computeFullPath()])

					val value = recipes.fromJsonTree(json)
					tasks.add(Entry(value, json, listedFile))
				} catch (err: Throwable) {
					LOGGER.error("Loading recipe definition file $listedFile", err)
				}
			}
		}
	}
}
