
@file:Suppress("unused")

package ru.dbotthepony.mc.otm.core

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableMultimap
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import net.minecraft.core.BlockPos
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtAccounter
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.chat.ComponentContents
import net.minecraft.network.chat.contents.TranslatableContents
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.entity.Entity
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.item.crafting.Ingredient
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateHolder
import net.minecraft.world.level.block.state.properties.Property
import net.minecraft.world.phys.Vec3
import net.minecraftforge.common.ForgeHooks
import net.minecraftforge.common.util.LazyOptional
import net.minecraftforge.fluids.FluidStack
import net.minecraftforge.items.IItemHandler
import net.minecraftforge.registries.ForgeRegistries
import net.minecraftforge.registries.ForgeRegistry
import net.minecraftforge.registries.IForgeRegistry
import ru.dbotthepony.mc.otm.core.math.Vector
import ru.dbotthepony.mc.otm.core.util.readInt
import ru.dbotthepony.mc.otm.core.util.readVarIntLE
import ru.dbotthepony.mc.otm.core.util.writeInt
import ru.dbotthepony.mc.otm.core.util.writeVarIntLE
import java.io.InputStream
import java.io.OutputStream
import java.lang.ref.Reference
import java.math.BigInteger
import java.util.Arrays
import java.util.Spliterators
import java.util.UUID
import java.util.function.Consumer
import java.util.stream.Stream
import java.util.stream.StreamSupport
import kotlin.reflect.KProperty

fun FriendlyByteBuf.writeBigInteger(value: BigInteger) {
	writeByteArray(value.toByteArray())
}

fun FriendlyByteBuf.readBigInteger(byteLimit: Int = 128) = BigInteger(readByteArray(byteLimit))

operator fun IItemHandler.get(index: Int): ItemStack = getStackInSlot(index)

operator fun JsonObject.set(s: String, value: JsonElement) = add(s, value)
operator fun JsonObject.set(s: String, value: String) = add(s, JsonPrimitive(value))
operator fun JsonObject.set(s: String, value: Number) = add(s, JsonPrimitive(value))
operator fun JsonObject.set(s: String, value: Boolean) = add(s, JsonPrimitive(value))

fun <T> LazyOptional<T>.orNull(): T? {
	if (!isPresent) {
		return null
	}

	return resolve().orElse(null)
}

fun <T> LazyOptional<T>.orThrow(): T {
	if (!isPresent) {
		throw IllegalStateException("Capability was expected to be not null")
	}

	return resolve().get() ?: throw IllegalStateException("Capability was expected to be not null")
}

inline fun <T> LazyOptional<T>.ifPresentK(lambda: (T) -> Unit) {
	if (isPresent) {
		val value = resolve().orElse(null) ?: throw IllegalStateException("Capability was expected to be not null")
		lambda.invoke(value)
	}
}

val ItemStack.tagNotNull: CompoundTag get() = orCreateTag

inline val FluidStack.isNotEmpty get() = !isEmpty
inline val ItemStack.isNotEmpty get() = !isEmpty

inline var Entity.position: Vec3
	get() = position()
	set(value) { setPos(value) }

inline val <reified T : Enum<T>> T.next: T get() {
	val values = enumValues<T>()
	val next = (ordinal + 1) % values.size
	return values[next]
}

inline val <reified T : Enum<T>> T.prev: T get() {
	val values = enumValues<T>()
	var next = ordinal - 1

	if (next < 0) {
		next = values.size - 1
	}

	return values[next]
}

fun <T : Enum<T>> T.next(values: Array<out T>): T {
	val next = (ordinal + 1) % values.size
	return values[next]
}

fun <T : Enum<T>> T.prev(values: Array<out T>): T {
	var next = ordinal - 1

	if (next < 0) {
		next = values.size - 1
	}

	return values[next]
}

inline fun <T : Any> immutableList(size: Int, initializer: (index: Int) -> T): ImmutableList<T> {
	require(size >= 0) { "Invalid list size $size" }

	return when (size) {
		0 -> ImmutableList.of()
		1 -> ImmutableList.of(initializer(0))
		else -> ImmutableList.Builder<T>().let {
			for (i in 0 until size) {
				it.add(initializer(i))
			}

			it.build()
		}
	}
}

inline fun <K : Any, V : Any> immutableMap(initializer: ImmutableMap.Builder<K, V>.() -> Unit): ImmutableMap<K, V> {
	val builder = ImmutableMap.Builder<K, V>()
	initializer.invoke(builder)
	return builder.build()
}

inline fun <K : Any, V : Any> immutableMultimap(initializer: ImmutableMultimap.Builder<K, V>.() -> Unit): ImmutableMultimap<K, V> {
	val builder = ImmutableMultimap.Builder<K, V>()
	initializer.invoke(builder)
	return builder.build()
}

inline fun <V : Any> immutableSet(initializer: Consumer<V>.() -> Unit): ImmutableSet<V> {
	val builder = ImmutableSet.Builder<V>()
	initializer.invoke(builder::add)
	return builder.build()
}

inline fun <V : Any> immutableList(initializer: Consumer<V>.() -> Unit): ImmutableList<V> {
	val builder = ImmutableList.Builder<V>()
	initializer.invoke(builder::add)
	return builder.build()
}

fun <T> IForgeRegistry<T>.getID(value: T): Int {
	return (this as ForgeRegistry<T>).getID(value)
}

fun <T> IForgeRegistry<T>.getValue(index: Int): T? {
	return (this as ForgeRegistry<T>).getValue(index)
}

fun IForgeRegistry<*>.getID(value: ResourceLocation): Int {
	return (this as ForgeRegistry<*>).getID(value)
}

fun FriendlyByteBuf.writeItemType(value: Item) {
	writeInt(ForgeRegistries.ITEMS.getID(value))
}

fun OutputStream.writeItemType(value: Item) {
	writeVarIntLE(ForgeRegistries.ITEMS.getID(value))
}

fun FriendlyByteBuf.readItemType(): Item {
	return ForgeRegistries.ITEMS.getValue(readInt()) ?: Items.AIR
}

fun InputStream.readItemType(sizeLimit: NbtAccounter? = null): Item {
	return ForgeRegistries.ITEMS.getValue(readVarIntLE(sizeLimit)) ?: Items.AIR
}

operator fun <T : Comparable<T>> StateHolder<*, *>.get(property: Property<T>): T {
	return getValue(property)
}

operator fun <S : StateHolder<*, *>, T : Comparable<T>> S.set(property: Property<T>, value: T): S {
	setValue(property, value)
	return this
}

fun <T> List<T>.toImmutableList(): List<T> {
	return ImmutableList.copyOf(this)
}

operator fun <R> (() -> R).getValue(thisRef: Any, property: KProperty<*>): R {
	return invoke()
}

fun BlockState.getExplosionResistance(level: BlockGetter, pos: BlockPos): Float {
	return try {
		getExplosionResistance(level, pos, null)
	} catch (err: NullPointerException) {
		block.explosionResistance
		// Потому что возможно какой-либо мод не ожидает что Explosion == null
		// особенно учитывая что интерфейс IForgeBlock не имеет @ParamsAreNonnullByDefault
		// и аргумент не помечен как @Nullable
		// тем самым имеет тип Explosion! который указывается как Explosion? .. Explosion!!
	} catch (err: IllegalArgumentException) {
		block.explosionResistance
	}
}

fun Level.gracefulBlockBreak(blockPos: BlockPos, block: BlockState = getBlockState(blockPos)) {
	val tile = getBlockEntity(blockPos)
	val state = block.fluidState.createLegacyBlock()
	block.onRemove(this, blockPos, state, false)
	Block.dropResources(block, this, blockPos, tile)
	block.block.destroy(this, blockPos, block)
	setBlock(blockPos, state, Block.UPDATE_ALL)
}

fun <E> MutableCollection<E>.addAll(elements: Iterator<E>) {
	for (item in elements) {
		add(item)
	}
}

fun <E> MutableCollection<E>.addAll(elements: Stream<out E>) {
	for (item in elements) {
		add(item)
	}
}

fun <E> Iterable<E>.stream(): Stream<out E> {
	return StreamSupport.stream(this.spliterator(), false)
}

fun <E> Iterator<E>.stream(): Stream<out E> {
	return StreamSupport.stream(Spliterators.spliteratorUnknownSize(this, 0), false)
}

val Ingredient.isActuallyEmpty: Boolean get() = ForgeHooks.hasNoElements(this)

fun Entity.genericPositions(): Collection<Vector> {
	return listOf(
		position,
		eyePosition,
		boundingBox.center
	)
}

fun ResourceLocation.toUUID(): UUID {
	return toString().toUUID()
}

fun String.toUUID(): UUID {
	return UUID.nameUUIDFromBytes(toByteArray(Charsets.UTF_8))
}

fun <T> Collection<T>.probablyParallelStream(): Stream<out T> {
	if (size >= 400) {
		// TODO: https://github.com/MinecraftForge/EventBus/issues/44
		return stream()
	}

	return stream()
}

fun <T> Array<T>.stream(): Stream<T> = Arrays.stream(this)

@Suppress("unchecked_cast")
fun <T> Stream<T?>.filterNotNull(): Stream<T> {
	return filter { it != null } as Stream<T>
}

@Suppress("unchecked_cast")
inline fun <reified T> Stream<*>.filterIsInstance(): Stream<T> {
	return filter { it is T } as Stream<T>
}

inline fun <T> MutableList<out Reference<out T>>.forValidRefs(fn: (T) -> Unit) {
	val iterator = listIterator()

	for (value in iterator) {
		val get = value.get()

		if (get == null) {
			iterator.remove()
		} else {
			fn.invoke(get)
		}
	}
}

inline fun <T> MutableList<out Reference<out T>>.forValidRefsBreak(fn: (T) -> Boolean) {
	val iterator = listIterator()

	for (value in iterator) {
		val get = value.get()

		if (get == null) {
			iterator.remove()
		} else {
			if (fn.invoke(get)) {
				break
			}
		}
	}
}

val ComponentContents.key: String
	get() = (this as? TranslatableContents ?: throw ClassCastException("$this is not a TranslatableContents")).key

fun <T : Comparable<T>> BlockState.getValueNullable(prop: Property<T>): T? {
	if (hasProperty(prop)) {
		return getValue(prop)
	}

	return null
}

fun <T> Stream<T>.asIterable(): Iterable<T> {
	return object : Iterable<T> {
		override fun iterator(): Iterator<T> {
			return this@asIterable.iterator()
		}
	}
}

// Kotlin type safety:
// since Java generics are invariant,
// and can not distinguish between null and non-null type parameters
// we need to tell compiler that Comparator.nullsFirst actually has next signature:
// fun <T> Comparator.nullsFirst(original: Comparator<in T>): Comparator<in T?>
fun <T> Comparator<in T>.nullsFirst(): Comparator<T?> {
	return Comparator.nullsFirst(this as Comparator<in T?>)
}

fun <T> Comparator<in T>.nullsLast(): Comparator<T?> {
	return Comparator.nullsLast(this as Comparator<in T?>)
}

/**
 * Returns applicable index to put [element] into [List] determined by [comparator], optionally specifying ranges as [fromIndex] and [toIndex]
 *
 * If [List] is not sorted, result of this function is undefined
 */
fun <E> List<E>.searchInsertionIndex(element: E, comparator: Comparator<E>, fromIndex: Int = 0, toIndex: Int = size): Int {
	require(toIndex >= fromIndex) { "Invalid range: to $toIndex >= from $fromIndex" }
	require(fromIndex >= 0) { "Invalid from index: $fromIndex" }
	require(toIndex >= 0) { "Invalid to index: $toIndex" }
	require(fromIndex <= size) { "Invalid from index: $fromIndex (list size $size)" }
	require(toIndex <= size) { "Invalid to index: $toIndex (list size $size)" }

	if (fromIndex == size || fromIndex == toIndex || comparator.compare(element, this[fromIndex]) <= 0) {
		return fromIndex
	}

	if (toIndex - fromIndex <= 10) {
		for (i in fromIndex + 1 until toIndex) {
			val compare = comparator.compare(element, this[i])

			if (compare <= 0) {
				return i
			}
		}

		return size
	} else {
		var lower = fromIndex
		var upper = toIndex - 1

		while (upper - lower >= 10) {
			val middle = (upper + lower) / 2
			val compare = comparator.compare(element, this[middle])

			if (compare == 0) {
				return middle
			} else if (compare < 0) {
				upper = middle
			} else {
				lower = middle
			}
		}

		return searchInsertionIndex(element, comparator, lower, upper + 1)
	}
}

/**
 * Inserts [element] into [MutableList] at index determined by [comparator]
 *
 * If [MutableList] is not sorted, result of this function is undefined
 */
fun <E> MutableList<E>.addSorted(element: E, comparator: Comparator<E>) {
	add(searchInsertionIndex(element, comparator), element)
}
