package ru.dbotthepony.mc.otm.core

import com.google.gson.JsonElement
import com.google.gson.JsonSyntaxException
import com.mojang.serialization.Codec
import com.mojang.serialization.DataResult
import com.mojang.serialization.JsonOps
import net.minecraft.core.BlockPos
import net.minecraft.core.Holder
import net.minecraft.core.Registry
import net.minecraft.core.RegistryAccess
import net.minecraft.core.Vec3i
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.core.registries.Registries
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.Tag
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.network.chat.contents.PlainTextContents
import net.minecraft.network.chat.contents.TranslatableContents
import net.minecraft.network.chat.contents.TranslatableContents.isAllowedPrimitiveArgument
import net.minecraft.resources.ResourceKey
import net.minecraft.resources.ResourceLocation
import net.minecraft.sounds.SoundEvent
import net.minecraft.tags.DamageTypeTags
import net.minecraft.world.damagesource.DamageSource
import net.minecraft.world.damagesource.DamageType
import net.minecraft.world.item.Item
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.material.Fluid
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import net.neoforged.neoforge.capabilities.BlockCapability
import ru.dbotthepony.mc.otm.core.util.readBinaryJson
import ru.dbotthepony.mc.otm.core.util.writeBinaryJson
import kotlin.jvm.optionals.getOrNull

// because doing it inline is ugly
fun <V : Any> Codec<V>.fromJson(value: JsonElement): V? {
	return decode(JsonOps.INSTANCE, value).result().map { left -> left.first }.getOrNull()
}

fun <V : Any> Codec<V>.fromJsonStrict(value: JsonElement): V {
	return decode(JsonOps.INSTANCE, value).mapOrElse({ left -> left.first }, { throw JsonSyntaxException("Error decoding element: ${it.message()}") })
}

fun <V : Any> Codec<V>.toJson(value: V, prefix: JsonElement = JsonOps.INSTANCE.empty()): JsonElement? {
	return encode(value, JsonOps.INSTANCE, prefix).mapOrNull { it }
}

fun <V : Any> Codec<V>.toJsonStrict(value: V, prefix: JsonElement = JsonOps.INSTANCE.empty()): JsonElement {
	return encode(value, JsonOps.INSTANCE, prefix).mapOrElse({ it }, { throw RuntimeException("Error encoding element: ${it.message()}") })
}

fun <V : Any> Codec<V>.fromNbt(value: Tag): V? {
	return decode(NbtOps.INSTANCE, value).mapOrNull { left -> left.first }
}

fun <V : Any> Codec<V>.fromNbtStrict(value: Tag): V {
	return decode(NbtOps.INSTANCE, value).mapOrElse({ left -> left.first }, { throw RuntimeException("Error decoding element: ${it.message()}") })
}

fun <V : Any> Codec<V>.toNbt(value: V, prefix: Tag = NbtOps.INSTANCE.empty()): Tag? {
	return encode(value, NbtOps.INSTANCE, prefix).mapOrNull { it }
}

fun <V : Any> Codec<V>.toNbtStrict(value: V, prefix: Tag = NbtOps.INSTANCE.empty()): Tag {
	return encode(value, NbtOps.INSTANCE, prefix).mapOrElse({ it }, { throw RuntimeException("Error encoding element: ${it.message()}") })
}

fun <V : Any> Codec<V>.toNetwork(buff: FriendlyByteBuf, value: V) {
	buff.writeBinaryJson(toJsonStrict(value))
}

fun <V : Any> Codec<V>.fromNetwork(buff: FriendlyByteBuf): V {
	return fromJsonStrict(buff.readBinaryJson())
}

// 1.19 being 1.19
fun TranslatableComponent(key: String, vararg values: Any): MutableComponent {
	// thanks NeoFrogge NeoForge NeonForges
	return MutableComponent.create(TranslatableContents(key, null, values.map { if (it !is Component && !isAllowedPrimitiveArgument(it)) it.toString() else it }.toTypedArray()))
}

fun TextComponent(value: String): MutableComponent = MutableComponent.create(PlainTextContents.create(value))

fun <T : Any> Registry<T>.getKeyNullable(value: T): ResourceLocation? {
	val key = getResourceKey(value)

	if (key.isPresent) {
		return key.get().location()
	}

	return null
}

val Item.registryName get() = BuiltInRegistries.ITEM.getKeyNullable(this)
val Fluid.registryName get() = BuiltInRegistries.FLUID.getKeyNullable(this)
val Block.registryName get() = BuiltInRegistries.BLOCK.getKeyNullable(this)

// 1.19.3 lol
inline val SoundEvent.holder: Holder<SoundEvent> get() = BuiltInRegistries.SOUND_EVENT.wrapAsHolder(this)

// 1.19.4 :thonkang:
inline val DamageSource.isFall get() = `is`(DamageTypeTags.IS_FALL)
inline val DamageSource.isBypassArmor get() = `is`(DamageTypeTags.BYPASSES_ARMOR)
inline val DamageSource.isExplosion get() = `is`(DamageTypeTags.IS_EXPLOSION)
inline val DamageSource.isFire get() = `is`(DamageTypeTags.IS_FIRE)

fun RegistryAccess.damageType(key: ResourceKey<DamageType>): Holder<DamageType> {
	return registryOrThrow(Registries.DAMAGE_TYPE).getHolderOrThrow(key)
}

// 1.21 :help_me:
fun ResourceLocation(namespace: String, path: String): ResourceLocation = ResourceLocation.fromNamespaceAndPath(namespace, path)

fun <IN, OUT> DataResult<IN>.mapOrNull(map: (IN) -> OUT): OUT? {
	return result().map(map).orElse(null)
}

fun AABB(mins: Vec3i, maxs: Vec3i): AABB {
	return AABB(Vec3.atLowerCornerOf(mins), Vec3.atLowerCornerOf(maxs))
}

@Deprecated("Unchecked capability cast")
fun <T, C> Level.getCapability(capability: BlockCapability<T, C>, blockPos: BlockPos): T? {
	return getCapability(capability as BlockCapability<T, Void>, blockPos)
}
