package ru.dbotthepony.mc.otm.data

import com.mojang.datafixers.util.Pair
import com.mojang.serialization.Codec
import com.mojang.serialization.DataResult
import com.mojang.serialization.DynamicOps
import com.mojang.serialization.JsonOps
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.world.item.crafting.Ingredient
import ru.dbotthepony.mc.otm.core.collect.allEqual
import ru.dbotthepony.mc.otm.core.collect.map
import ru.dbotthepony.mc.otm.core.collect.toList
import ru.dbotthepony.mc.otm.core.collect.toStream
import ru.dbotthepony.mc.otm.core.stream
import ru.dbotthepony.mc.otm.recipe.IIngredientMatrix
import ru.dbotthepony.mc.otm.recipe.IngredientMatrix
import java.util.function.Supplier

class IngredientMatrixCodec(ingredientCodec: Codec<Ingredient>) : Codec<IIngredientMatrix> {
	private val ingredientList = Codec.list(ingredientCodec)
	private val doubleIngredientList = Codec.list(ingredientList)

	override fun <T : Any> encode(input: IIngredientMatrix, ops: DynamicOps<T>, prefix: T): DataResult<T> {
		return doubleIngredientList.encode(
			(0 until input.height).iterator().map { row ->
				(0 until input.width).iterator().map { column ->
					input[column, row]
				}.toList()
			}.toList(),
			ops,
			prefix
		)
	}

	private data class Handwritten(val pattern: List<String>, val key: Map<Char, Ingredient>)

	private val handwrittenCodec = RecordCodecBuilder.create<Handwritten> {
		it.group(
			Codec.list(Codec.STRING)
				.flatXmap(
					{ DataResult.success(it) },
					{ if (it.iterator().map { it.length }.allEqual()) DataResult.success(it) else DataResult.error { "One or more of patten strings differ in length" } }
				)
				.fieldOf("pattern").forGetter(Handwritten::pattern),
			Codec.unboundedMap(
				Codec.STRING
					.flatXmap(
						{ if (it.length == 1) DataResult.success(it[0]) else DataResult.error { "Ingredient key must be exactly 1 symbol in length, '$it' is invalid" } },
						{ DataResult.success(it.toString()) }
					), ingredientCodec).fieldOf("key").forGetter(Handwritten::key)
		).apply(it, ::Handwritten)
	}

	override fun <T : Any> decode(ops: DynamicOps<T>, input: T): DataResult<Pair<IIngredientMatrix, T>> {
		return ops.getList(input).get().map(
			{
				val lines = ArrayList<DataResult<List<Ingredient>>>()

				it.accept {
					lines.add(ingredientList.decode(ops, it).map { it.first })
				}

				val errors = ArrayList<Supplier<String>>()
				val ingredients = ArrayList<List<Ingredient>>()

				lines.withIndex().forEach {
					val (line, result) = it
					result.get().map({ ingredients.add(it) }, { errors.add { "Line $line: ${it.message()}" } })
				}

				if (errors.isNotEmpty()) {
					DataResult.error { "Failed to decode ingredient matrix: ${errors.joinToString { it.get() }}" }
				} else if (ingredients.isEmpty()) {
					DataResult.error { "Ingredient list is empty" }
				} else if (!ingredients.iterator().map { it.size }.allEqual()) {
					DataResult.error { "Ingredient list is not a matrix (one or multiple of rows are mismatched size)" }
				} else {
					val result = IngredientMatrix(ingredients.first().size, ingredients.size)

					for ((row, columns) in ingredients.withIndex()) {
						for ((column, ingredient) in columns.withIndex()) {
							result[column, row] = ingredient
						}
					}

					DataResult.success(Pair(result, ops.empty()))
				}
			},
			{ err1 ->
				handwrittenCodec.decode(ops, input).get().map(
					{
						DataResult.success(it)
					},
					{
						DataResult.error { "Failed to decode ingredients as list: ${err1.message()} and as pattern/dictionary: ${it.message()}" }
					}
				).flatMap {
					val handwritten = it.first
					val result = IngredientMatrix(handwritten.pattern.first().length, handwritten.pattern.size)

					for ((row, pattern) in handwritten.pattern.withIndex()) {
						for ((column, symbol) in pattern.withIndex()) {
							val ingredient = if (symbol == ' ') handwritten.key[symbol] ?: Ingredient.EMPTY else handwritten.key[symbol] ?: return@flatMap DataResult.error { "Unknown ingredient with index '$symbol'" }
							result[column, row] = ingredient
 						}
					}

					DataResult.success(Pair(result, ops.empty()))
				}
			}
		)
	}
}
