package ru.dbotthepony.mc.otm.matter

import com.google.common.collect.ImmutableList
import com.mojang.datafixers.util.Either
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.codecs.RecordCodecBuilder
import net.minecraft.resources.ResourceLocation
import net.minecraft.tags.TagKey
import net.minecraft.world.item.Item
import net.minecraftforge.registries.ForgeRegistries
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.data.DecimalCodec
import ru.dbotthepony.mc.otm.data.PredicatedCodecList
import java.util.Optional
import java.util.function.Predicate

class ComputeAction(
	id: Either<ResourceLocation, TagKey<Item>>,
	values: Collection<Source>,
	priority: Optional<Int> = Optional.empty(),
) : AbstractRegistryAction(id, priority = priority) {
	companion object : Type<ComputeAction> {
		private val constantCodecFull: kotlin.Pair<Codec<Constant>, Predicate<Constant>> by lazy {
			RecordCodecBuilder.create<Constant> {
				it.group(
					DecimalCodec.fieldOf("matter").forGetter(Constant::matter),
					Codec.DOUBLE.fieldOf("complexity").forGetter(Constant::complexity),
					IMatterFunction.registry.codec.fieldOf("matterFunction").forGetter(Constant::matterFunction),
					IMatterFunction.registry.codec.fieldOf("complexityFunction").forGetter(Constant::complexityFunction),
				).apply(it, ::Constant)
			} to Predicate { true }
		}

		private val constantCodecValues: kotlin.Pair<Codec<Constant>, Predicate<Constant>> by lazy {
			RecordCodecBuilder.create<Constant> {
				it.group(
					DecimalCodec.fieldOf("matter").forGetter(Constant::matter),
					Codec.DOUBLE.fieldOf("complexity").forGetter(Constant::complexity),
					IMatterFunction.registry.codec.fieldOf("function").forGetter(Constant::matterFunction),
				).apply(it) { a, b, c -> Constant(a, b, c, c) }
			} to Predicate { it.matterFunction == it.complexityFunction }
		}

		private val constantCodecFunctions: kotlin.Pair<Codec<Constant>, Predicate<Constant>> by lazy {
			RecordCodecBuilder.create<Constant> {
				it.group(
					DecimalCodec.fieldOf("value").forGetter(Constant::matter),
					IMatterFunction.registry.codec.fieldOf("matterFunction").forGetter(Constant::matterFunction),
					IMatterFunction.registry.codec.fieldOf("complexityFunction").forGetter(Constant::complexityFunction),
				).apply(it) { a, b, c -> Constant(a, a.toDouble(), b, c) }
			} to Predicate { it.matter == Decimal(it.complexity.toString()) }
		}

		private val constantCodecConcise: kotlin.Pair<Codec<Constant>, Predicate<Constant>> by lazy {
			RecordCodecBuilder.create<Constant> {
				it.group(
					DecimalCodec.fieldOf("value").forGetter(Constant::matter),
					IMatterFunction.registry.codec.fieldOf("function").forGetter(Constant::matterFunction),
				).apply(it) { a, b -> Constant(a, a.toDouble(), b, b) }
			} to Predicate { it.matter == Decimal(it.complexity.toString()) && it.matterFunction == it.complexityFunction }
		}

		private val constantCodecComplexity: kotlin.Pair<Codec<Constant>, Predicate<Constant>> by lazy {
			RecordCodecBuilder.create<Constant> {
				it.group(
					Codec.DOUBLE.fieldOf("complexity").forGetter(Constant::complexity),
					IMatterFunction.registry.codec.fieldOf("function").forGetter(Constant::complexityFunction),
				).apply(it) { a, b -> Constant(Decimal.ZERO, a.toDouble(), IMatterFunction.NOOP, b) }
			} to Predicate { it.matterFunction == IMatterFunction.NOOP }
		}

		private val constantCodecComplexity2: kotlin.Pair<Codec<Constant>, Predicate<Constant>> by lazy {
			RecordCodecBuilder.create<Constant> {
				it.group(
					Codec.DOUBLE.fieldOf("complexity").forGetter(Constant::complexity),
					IMatterFunction.registry.codec.fieldOf("complexityFunction").forGetter(Constant::complexityFunction),
				).apply(it) { a, b -> Constant(Decimal.ZERO, a.toDouble(), IMatterFunction.NOOP, b) }
			} to Predicate { it.matterFunction == IMatterFunction.NOOP }
		}

		private val constantCodecMatter: kotlin.Pair<Codec<Constant>, Predicate<Constant>> by lazy {
			RecordCodecBuilder.create<Constant> {
				it.group(
					DecimalCodec.fieldOf("matter").forGetter(Constant::matter),
					IMatterFunction.registry.codec.fieldOf("function").forGetter(Constant::matterFunction),
				).apply(it) { a, b -> Constant(a, 0.0, b, IMatterFunction.NOOP) }
			} to Predicate { it.complexityFunction == IMatterFunction.NOOP }
		}

		private val constantCodecMatter2: kotlin.Pair<Codec<Constant>, Predicate<Constant>> by lazy {
			RecordCodecBuilder.create<Constant> {
				it.group(
					DecimalCodec.fieldOf("matter").forGetter(Constant::matter),
					IMatterFunction.registry.codec.fieldOf("matterFunction").forGetter(Constant::matterFunction),
				).apply(it) { a, b -> Constant(a, 0.0, b, IMatterFunction.NOOP) }
			} to Predicate { it.complexityFunction == IMatterFunction.NOOP }
		}

		private data class StringConstantCodec(val fn: IMatterFunction, val symbol: Char) : Codec<Constant>, Predicate<Constant> {
			val pair = this to this

			override fun <T : Any> encode(input: Constant, ops: DynamicOps<T>, prefix: T): DataResult<T> {
				return DataResult.success(ops.createString(symbol + input.matter.toString()))
			}

			override fun <T : Any> decode(ops: DynamicOps<T>, input: T): DataResult<Pair<Constant, T>> {
				return ops.getStringValue(input).flatMap {
					if (it[0] == symbol) {
						try {
							DataResult.success(Pair(Constant(Decimal(it.substring(1).trim()), it.substring(1).trim().toDouble(), fn, fn), ops.empty()))
						} catch (err: NumberFormatException) {
							DataResult.error { "Not a number: ${it.substring(1).trim()} (input string: $it)" }
						}
					} else {
						DataResult.error { "Input string does not match expected operand: expected $symbol, got ${it[0]} (for input $it)" }
					}
				}
			}

			override fun test(t: Constant): Boolean {
				return t.matterFunction == fn && t.complexityFunction == fn && t.matter == Decimal(t.complexity.toString())
			}
		}

		private object PlainStringConstantCodec : Codec<Constant>, Predicate<Constant> {
			override fun <T : Any> encode(input: Constant, ops: DynamicOps<T>, prefix: T): DataResult<T> {
				return DataResult.success(ops.createString(input.matter.toString()))
			}

			override fun <T : Any> decode(ops: DynamicOps<T>, input: T): DataResult<Pair<Constant, T>> {
				val result = ops.getNumberValue(input).flatMap {
					try {
						DataResult.success(Pair(Constant(Decimal(it.toString()), it.toDouble(), IMatterFunction.PLUS, IMatterFunction.PLUS), ops.empty()))
					} catch (err: NumberFormatException) {
						DataResult.error { "Not a number: $it" }
					}
				}

				if (result.result().isPresent)
					return result

				return ops.getStringValue(input).flatMap {
					try {
						DataResult.success(Pair(Constant(Decimal(it), it.toDouble(), IMatterFunction.PLUS, IMatterFunction.PLUS), ops.empty()))
					} catch (err: NumberFormatException) {
						DataResult.error { "Not a number: $it" }
					}
				}
			}

			override fun test(t: Constant): Boolean {
				return t.matterFunction == IMatterFunction.PLUS && t.complexityFunction == IMatterFunction.PLUS && t.matter == Decimal(t.complexity.toString())
			}
		}

		private val constantCodec: Codec<Constant> by lazy {
			PredicatedCodecList(
				PlainStringConstantCodec to PlainStringConstantCodec,
				StringConstantCodec(IMatterFunction.DIV, '/').pair,
				StringConstantCodec(IMatterFunction.MUL, '*').pair,
				StringConstantCodec(IMatterFunction.AT_LEAST, '>').pair,
				StringConstantCodec(IMatterFunction.AT_MOST, '<').pair,
				StringConstantCodec(IMatterFunction.MINUS, '-').pair,
				StringConstantCodec(IMatterFunction.MOD, '%').pair,
				StringConstantCodec(IMatterFunction.REPLACE, '^').pair,
				constantCodecMatter,
				constantCodecMatter2,
				constantCodecComplexity,
				constantCodecComplexity2,
				constantCodecConcise,
				constantCodecFunctions,
				constantCodecValues,
				constantCodecFull,
			)
		}

		private val valueCodecConcise: Codec<Value> by lazy {
			RecordCodecBuilder.create {
				it.group(
					TargetCodec.fieldOf("id").forGetter(Value::id),
					IMatterFunction.registry.codec.fieldOf("function").forGetter(Value::matterFunction),
				).apply(it, ::Value)
			}
		}

		private val valueCodecVerbose: Codec<Value> by lazy {
			RecordCodecBuilder.create {
				it.group(
					TargetCodec.fieldOf("id").forGetter(Value::id),
					IMatterFunction.registry.codec.fieldOf("matterFunction").forGetter(Value::matterFunction),
					IMatterFunction.registry.codec.fieldOf("complexityFunction").forGetter(Value::complexityFunction),
				).apply(it, ::Value)
			}
		}

		private val valueCodec: Codec<Value> by lazy {
			Codec.either(valueCodecConcise, valueCodecVerbose)
				.xmap({ it.map({ it }, { it }) }, { if (it.matterFunction == it.complexityFunction) Either.left(it) else Either.right(it) })
		}

		private object ActualValueCodec : Codec<Value> {
			override fun <T : Any> encode(input: Value, ops: DynamicOps<T>, prefix: T): DataResult<T> {
				if (input.matterFunction == input.complexityFunction && input.matterFunction == IMatterFunction.PLUS) {
					return TargetCodec.encode(input.id, ops, prefix)
				} else {
					return valueCodec.encode(input, ops, prefix)
				}
			}

			override fun <T : Any> decode(ops: DynamicOps<T>, input: T): DataResult<Pair<Value, T>> {
				val result = TargetCodec.decode(ops, input)

				if (result.result().isPresent) {
					return result.map { it.mapFirst { Value(it, IMatterFunction.PLUS) } }
				}

				return valueCodec.decode(ops, input)
			}
		}

		override val codec: Codec<ComputeAction> by lazy {
			RecordCodecBuilder.create {
				it.group(
					TargetCodec.fieldOf("id").forGetter(ComputeAction::id),
					Codec.list(Codec
						.either(constantCodec, ActualValueCodec)
						.xmap({ it.map({ it }, { it }) }, { if (it is Constant) Either.left(it) else Either.right(it as Value) }))
						.fieldOf("values")
						.forGetter(ComputeAction::values),
					Codec.INT.optionalFieldOf("prority").forGetter(ComputeAction::priority)
				).apply(it, ::ComputeAction)
			}
		}
	}

	override val type: Type<*>
		get() = Companion

	sealed class Source(
		val matterFunction: IMatterFunction,
		val complexityFunction: IMatterFunction
	) {
		internal abstract fun provideValues(): MatterManager.Result

		internal fun update(self: IMatterValue): MatterManager.Result {
			val grab = provideValues()

			if (grab.value == null) {
				return grab
			}

			return MatterManager.Result(MatterValue(
				matterFunction.updateValue(self.matter, grab.value.matter),
				complexityFunction.updateValue(self.complexity, grab.value.complexity)
			))
		}
	}

	class Constant(
		val matter: Decimal,
		val complexity: Double,
		matterFunction: IMatterFunction,
		complexityFunction: IMatterFunction = matterFunction,
	) : Source(matterFunction, complexityFunction) {
		constructor(
			value: Double,
			matterFunction: IMatterFunction,
			complexityFunction: IMatterFunction = matterFunction
		) : this(Decimal(value.toString()), value, matterFunction, complexityFunction)

		override fun provideValues(): MatterManager.Result {
			return MatterManager.Result(MatterValue(matter, complexity))
		}
	}

	class Value(
		val id: Either<ResourceLocation, TagKey<Item>>,
		matterFunction: IMatterFunction,
		complexityFunction: IMatterFunction = matterFunction,
	) : Source(matterFunction, complexityFunction) {
		override fun provideValues(): MatterManager.Result {
			return id.map(
				{
					MatterManager.compute(ForgeRegistries.ITEMS.getValue(it) ?: throw NullPointerException("$it does not point to anything"))
				},
				{
					MatterManager.compute(it)
				}
			)
		}
	}

	val values: ImmutableList<Source> = ImmutableList.copyOf(values)

	internal fun tryCompute(): MatterManager.Result {
		var compute: IMatterValue = IMatterValue.Companion

		for (fn in values) {
			val result = fn.update(compute)

			if (result.value == null) {
				return result
			} else {
				compute = result.value
			}
		}

		return MatterManager.Result(compute)
	}

	override fun doUpdate(registry: MutableCollection<IMutableRegistryEntry>, source: ResourceLocation) {
		// do nothing
	}
}
