package ru.dbotthepony.mc.otm.core.math

import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import net.minecraft.nbt.ByteArrayTag
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtAccounter
import net.minecraft.nbt.StringTag
import net.minecraft.nbt.Tag
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.util.RandomSource
import net.minecraftforge.common.ForgeConfigSpec
import ru.dbotthepony.mc.otm.config.ObservedConfigValue
import ru.dbotthepony.mc.otm.core.util.readVarIntLE
import ru.dbotthepony.mc.otm.core.util.writeVarIntLE
import java.io.InputStream
import java.io.OutputStream
import java.math.BigDecimal
import java.math.BigInteger
import java.math.MathContext
import java.math.RoundingMode

private val BI_MINUS_ONE = -BigInteger.ONE
private val PERCENTAGE_CONTEXT = MathContext(6) // 6 ибо это число знаков для вычисления процента

fun Decimal(value: Byte) = Decimal.valueOf(value)
fun Decimal(value: Short) = Decimal.valueOf(value)
fun Decimal(value: Int) = Decimal.valueOf(value)
fun Decimal(value: Long) = Decimal.valueOf(value)

/**
 * Fixed point arbitrary precision Decimal value. Values of this class embed [BigInteger] unscaled value, scale
 * is defined at compile time by [PRECISION].
 */
class Decimal private constructor(val mag: BigInteger, marker: Nothing?) : Number(), Comparable<Decimal> {
	constructor(value: BigInteger) : this(value * PRECISION_POW_BI, null)
	constructor(value: BigDecimal) : this(value.setScale(PRECISION, RoundingMode.HALF_UP).unscaledValue(), null)
	constructor(value: Float) : this(BigDecimal.valueOf(value.toDouble()))
	constructor(value: Double) : this(BigDecimal(value))
	constructor(value: String) : this(BigDecimal(value))

	private var _whole: BigInteger? = null
	private var _fractional: BigInteger? = null

	private fun computeWholeFractional(): BigInteger {
		val (a, b) = mag.divideAndRemainder(PRECISION_POW_BI)
		_whole = a
		_fractional = b
		return a
	}

	/**
	 * Whole part of this Decimal
	 */
	val whole: BigInteger get() {
		return _whole ?: computeWholeFractional()
	}

	/**
	 * Arbitrary fractional part of this Decimal, as [BigInteger]
	 *
	 * Makes sense only when utilized along [PRECISION], [PRECISION_POW], [PRECISION_POW_BI]
	 */
	val fractional: BigInteger get() {
		if (_fractional == null) computeWholeFractional()
		return _fractional!!
	}

	/**
	 * *Signed* normalized (-1,1) fractional part of this Decimal, as [Float]
	 */
	inline val fractionalFloat: Float get() {
		return fractional.toFloat() / PRECISION_FLOAT
	}

	/**
	 * *Signed* normalized (-1,1) fractional part of this Decimal, as [Double]
	 */
	inline val fractionalDouble: Double get() {
		return fractional.toDouble() / PRECISION_DOUBLE
	}

	override fun compareTo(other: Decimal): Int {
		return mag.compareTo(other.mag)
	}

	override fun toByte(): Byte {
		return whole.toByte()
	}

	override fun toChar(): Char {
		return whole.toChar()
	}

	override fun toDouble(): Double {
		return mag.toDouble() / PRECISION_DOUBLE
	}

	override fun toFloat(): Float {
		return mag.toFloat() / PRECISION_FLOAT
	}

	override fun toInt(): Int {
		if (whole > BI_INT_MAX) {
			return Int.MAX_VALUE
		} else if (whole < BI_INT_MIN) {
			return Int.MIN_VALUE
		} else {
			return whole.toInt()
		}
	}

	override fun toLong(): Long {
		if (whole > BI_LONG_MAX) {
			return Long.MAX_VALUE
		} else if (whole < BI_LONG_MIN) {
			return Long.MIN_VALUE
		} else {
			return whole.toLong()
		}
	}

	override fun toShort(): Short {
		return whole.toShort()
	}

	operator fun plus(other: Decimal): Decimal {
		if (other.mag.signum() == 0) {
			return this
		}

		return Decimal(mag + other.mag, null)
	}

	operator fun minus(other: Decimal): Decimal {
		if (other.mag.signum() == 0) {
			return this
		}

		return Decimal(mag - other.mag, null)
	}

	operator fun times(other: Decimal): Decimal {
		if (other.mag.signum() == 0) {
			return ZERO
		} else if (other == ONE) {
			return this
		} else if (other == MINUS_ONE) {
			return Decimal(-mag, null)
		}

		val result = mag * other.mag
		val (a, b) = result.divideAndRemainder(PRECISION_POW_BI)

		if (b >= PRECISION_POW_BI_HIGH) {
			return Decimal(a + BigInteger.ONE, null)
		} else {
			return Decimal(a, null)
		}
	}

	operator fun div(other: Decimal): Decimal {
		if (other.mag.signum() == 0) {
			throw ArithmeticException("Attempt to divide $this by zero")
		} else if (other == ONE) {
			return this
		} else if (other == MINUS_ONE) {
			return Decimal(-mag, null)
		}

		return Decimal((mag * PRECISION_POW_BI) / other.mag, null)
	}

	operator fun rem(other: Decimal): Decimal {
		return Decimal(mag % other.mag, null)
	}

	// Primitive operators
	operator fun plus(other: Float): Decimal {
		if (other == 0f) {
			return this
		} else if (other.isNaN()) {
			throw ArithmeticException("Attempt to add NaN to $this")
		} else if (other.isInfinite()) {
			throw ArithmeticException("Attempt to add infinity to $this")
		}

		return plus(Decimal(other))
	}

	operator fun minus(other: Float): Decimal {
		if (other == 0f) {
			return this
		} else if (other.isNaN()) {
			throw ArithmeticException("Attempt to subtract NaN from $this")
		} else if (other.isInfinite()) {
			throw ArithmeticException("Attempt to subtract infinity from $this")
		}

		return minus(Decimal(other))
	}

	operator fun times(other: Float): Decimal {
		if (other == 1f) {
			return this
		} else if (other == 0f) {
			return ZERO
		} else if (other == -1f) {
			return -this
		} else if (other.isNaN()) {
			throw ArithmeticException("Attempt to multiply $this by NaN")
		} else if (other.isInfinite()) {
			throw ArithmeticException("Attempt to multiply $this by infinity")
		}

		return times(Decimal(other))
	}

	operator fun div(other: Float): Decimal {
		if (other == 0f) {
			throw ArithmeticException("Attempt to divide $this by zero")
		} else if (other == 1f) {
			return this
		} else if (other == -1f) {
			return -this
		} else if (other.isNaN()) {
			throw ArithmeticException("Attempt to divide $this by NaN")
		} else if (other.isInfinite()) {
			throw ArithmeticException("Attempt to divide $this by infinity")
		}

		return div(Decimal(other))
	}

	operator fun plus(other: Double): Decimal {
		if (other == 0.0) {
			return this
		} else if (other.isNaN()) {
			throw ArithmeticException("Attempt to add NaN to $this")
		} else if (other.isInfinite()) {
			throw ArithmeticException("Attempt to add infinity to $this")
		}

		return plus(Decimal(other))
	}

	operator fun minus(other: Double): Decimal {
		if (other == 0.0) {
			return this
		} else if (other.isNaN()) {
			throw ArithmeticException("Attempt to subtract NaN from $this")
		} else if (other.isInfinite()) {
			throw ArithmeticException("Attempt to subtract infinity from $this")
		}

		return minus(Decimal(other))
	}

	operator fun times(other: Double): Decimal {
		if (other == 1.0) {
			return this
		} else if (other == 0.0) {
			return ZERO
		} else if (other == -1.0) {
			return -this
		} else if (other.isNaN()) {
			throw ArithmeticException("Attempt to multiply $this by NaN")
		} else if (other.isInfinite()) {
			throw ArithmeticException("Attempt to multiply $this by infinity")
		}

		return times(Decimal(other))
	}

	operator fun div(other: Double): Decimal {
		if (other == 0.0) {
			throw ArithmeticException("Attempt to divide $this by zero")
		} else if (other == 1.0) {
			return this
		} else if (other == -1.0) {
			return -this
		} else if (other.isNaN()) {
			throw ArithmeticException("Attempt to divide $this by NaN")
		} else if (other.isInfinite()) {
			throw ArithmeticException("Attempt to divide $this by infinity")
		}

		return div(Decimal(other))
	}

	operator fun plus(other: Int): Decimal {
		if (other == 0) {
			return this
		}

		return plus(valueOf(other))
	}

	operator fun minus(other: Int): Decimal {
		if (other == 0) {
			return this
		}

		return minus(valueOf(other))
	}

	operator fun times(other: Int): Decimal {
		if (other == 1) {
			return this
		} else if (other == 0) {
			return ZERO
		} else if (other == -1) {
			return -this
		}

		return times(valueOf(other))
	}

	operator fun div(other: Int): Decimal {
		if (other == 0) {
			throw ArithmeticException("Attempt to divide $this by zero")
		} else if (other == 1) {
			return this
		} else if (other == -1) {
			return -this
		}

		return div(valueOf(other))
	}

	operator fun plus(other: Long): Decimal {
		if (other == 0L) {
			return this
		}

		return plus(valueOf(other))
	}

	operator fun minus(other: Long): Decimal {
		if (other == 0L) {
			return this
		}

		return minus(valueOf(other))
	}

	operator fun times(other: Long): Decimal {
		if (other == 1L) {
			return this
		} else if (other == 0L) {
			return ZERO
		} else if (other == -1L) {
			return -this
		}

		return times(valueOf(other))
	}

	operator fun div(other: Long): Decimal {
		if (other == 0L) {
			throw ArithmeticException("Attempt to divide $this by zero")
		} else if (other == 1L) {
			return this
		} else if (other == -1L) {
			return -this
		}

		return div(valueOf(other))
	}

	operator fun plus(other: BigInteger): Decimal {
		if (other == BigInteger.ZERO) {
			return this
		}

		return plus(Decimal(other))
	}

	operator fun minus(other: BigInteger): Decimal {
		if (other == BigInteger.ZERO) {
			return this
		}

		return minus(Decimal(other))
	}

	operator fun times(other: BigInteger): Decimal {
		if (other == BigInteger.ONE) {
			return this
		} else if (other.signum() == 0) {
			return ZERO
		} else if (other == BI_MINUS_ONE) {
			return -this
		}

		return times(Decimal(other))
	}

	operator fun div(other: BigInteger): Decimal {
		if (other == BigInteger.ZERO) {
			throw ArithmeticException("Attempt to divide $this by zero")
		} else if (other == BigInteger.ONE) {
			return this
		} else if (other == BI_MINUS_ONE) {
			return -this
		}

		return div(Decimal(other))
	}
	// /Primitive operators

	operator fun unaryMinus(): Decimal {
		if (mag == BigInteger.ZERO) {
			return this
		}

		return Decimal(-mag, null)
	}

	operator fun unaryPlus() = this

	/**
	 * Sign number of this Decimal, as defined by [BigInteger.signum]
	 */
	fun signum() = mag.signum()

	/**
	 * Whenever this Decimal is negative (less than zero)
	 */
	inline val isNegative get() = signum() < 0

	/**
	 * Whenever this Decimal is positive (bigger than zero)
	 */
	inline val isPositive get() = signum() > 0

	/**
	 * Whenever this Decimal is zero
	 */
	inline val isZero get() = signum() == 0

	/**
	 * Alias for [coerceAtLeast] with [ZERO] constant, except this method might
	 * perform faster.
	 */
	fun moreThanZero(): Decimal {
		if (signum() >= 0)
			return this

		return ZERO
	}

	/**
	 * Alias for [coerceAtMost] with [ZERO] constant, except this method might
	 * perform faster.
	 */
	fun lessOrZero(): Decimal {
		if (signum() <= 0)
			return this

		return ZERO
	}

	fun toByteArray(): ByteArray {
		return mag.toByteArray()
	}

	fun serializeNBT(): Tag {
		return ByteArrayTag(toByteArray())
	}

	fun write(buff: FriendlyByteBuf) {
		buff.writeByteArray(toByteArray())
	}

	val absoluteValue: Decimal
		get() {
			if (isNegative) {
				return -this
			} else {
				return this
			}
		}

	/**
	 * Truncates fractional part of this Decimal
	 */
	fun floor(): Decimal {
		return Decimal(whole)
	}

	override fun equals(other: Any?): Boolean {
		return this === other || other is Decimal && mag == other.mag
	}

	override fun hashCode(): Int {
		return mag.hashCode()
	}

	fun toString(decimals: Int): String {
		if (decimals == 0) {
			return whole.toString()
		}

		if (mag.signum() == 0) {
			if (decimals < 0) {
				return "0.0"
			} else {
				return "0." + "0".repeat(decimals)
			}
		}

		var original = mag.toString()

		if (mag.signum() < 0) {
			// если у нас отрицательное число - убираем знак минуса
			original = original.substring(1)
		}

		if (original.length <= PRECISION) {
			// если у нас чисто дробное число - дописываем нули в начало
			original = "0".repeat(PRECISION - original.length + 1) + original
		}

		// теперь у нас беззнаковая строка с нужной длиной

		if (decimals < 0) {
			// нам неважно количество знаков после запятой
			var result = original.substring(0, original.length - PRECISION) + "." + original.substring(original.length - PRECISION)

			if (mag.signum() < 0) {
				result = "-$result"
			}

			var pos = result.length - 1

			while (result[pos] == '0') {
				if (result[pos - 1] == '0' || result[pos - 1] != '.')
					pos--
				else
					break
			}

			return result.substring(0, pos + 1)
		} else {
			// нужно некоторое количество знаков после запятой
			val result = original.substring(0, original.length - PRECISION) + "."
			var decimalPart = original.substring(original.length - PRECISION)

			if (decimalPart.length < decimals) {
				decimalPart += "0".repeat(decimals - decimalPart.length)
			} else if (decimalPart.length > decimals) {
				decimalPart = decimalPart.substring(0, decimals)
			}

			if (mag.signum() < 0) {
				return "-$result$decimalPart"
			} else {
				return "$result$decimalPart"
			}
		}
	}

	override fun toString(): String = toString(-1)

	fun toBigDecmial(): BigDecimal {
		return BigDecimal(mag, PRECISION)
	}

	fun percentage(divisor: Decimal, zeroing: Boolean = true): Float {
		if ((isZero || divisor.isZero) && zeroing) return 0f

		if (this >= divisor)
			return 1f

		if (this <= FLOAT_MAX_VALUE && divisor <= FLOAT_MAX_VALUE)
			return (toDouble() / divisor.toDouble()).toFloat()

		return toBigDecmial().divide(divisor.toBigDecmial(), PERCENTAGE_CONTEXT).toFloat()
	}

	@Suppress("unused")
	companion object {
		/**
		 * Amount of digits after fixed point, in 10 radix
		 */
		const val PRECISION = 11

		@JvmField
		val PRECISION_POW_BI: BigInteger = BigInteger("1" + "0".repeat(PRECISION))
		@JvmField
		val PRECISION_DOUBLE = PRECISION_POW_BI.toDouble()
		@JvmField
		val PRECISION_FLOAT = PRECISION_POW_BI.toFloat()
		@JvmField
		val PRECISION_POW_BI_NEGATIVE: BigInteger = -PRECISION_POW_BI
		@JvmField
		val PRECISION_POW_BI_HIGH: BigInteger = PRECISION_POW_BI / BigInteger.TWO

		private const val cacheSize = 16384
		private const val cacheSizeL = cacheSize.toLong()

		private val cache = Array(cacheSize) { Decimal(BigInteger.valueOf(it.toLong() - cacheSize / 2)) }

		/**
		 * Returns pooled value if present, otherwise constructs new object
		 */
		@JvmStatic
		fun valueOf(value: Int): Decimal {
			if (value in (cacheSize / -2) until cacheSize / 2) {
				return cache[value + cacheSize / 2]
			}

			return Decimal(BigInteger.valueOf(value.toLong()))
		}

		/**
		 * Returns pooled value if present, otherwise constructs new object
		 */
		@JvmStatic
		fun valueOf(value: Long): Decimal {
			if (value in (cacheSizeL / -2L) until cacheSizeL / 2L) {
				return cache[value.toInt() + cacheSize / 2]
			}

			return Decimal(BigInteger.valueOf(value))
		}

		/**
		 * Returns pooled value if present, otherwise constructs new object
		 */
		@JvmStatic
		fun valueOf(value: Short) = valueOf(value.toInt())

		/**
		 * Returns pooled value if present, otherwise constructs new object
		 */
		@JvmStatic
		fun valueOf(value: Byte) = valueOf(value.toInt())

		@JvmStatic fun valueOf(value: BigInteger) = Decimal(value)
		@JvmStatic fun valueOf(value: BigDecimal) = Decimal(value)
		@JvmStatic fun valueOf(value: String) = Decimal(value)
		@JvmStatic fun valueOf(value: Float) = Decimal(value)
		@JvmStatic fun valueOf(value: Double) = Decimal(value)

		@JvmStatic
		fun fromByteArray(input: ByteArray): Decimal {
			if (input.isEmpty()) {
				return ZERO
			} else {
				return Decimal(BigInteger(input), null)
			}
		}

		@JvmStatic
		fun deserializeNBT(input: Tag?): Decimal {
			if (input is ByteArrayTag) {
				return fromByteArray(input.asByteArray)
			} else if (input is StringTag) {
				return Decimal(input.asString)
			}

			return ZERO
		}

		@JvmStatic
		fun read(buff: FriendlyByteBuf): Decimal {
			return Decimal(BigInteger(buff.readByteArray()))
		}

		/**
		 * Takes in [value] as-is and constructs [Decimal] out of it ([value] is treated as already scaled by [PRECISION])
		 */
		@JvmStatic
		fun raw(value: BigInteger): Decimal = Decimal(value, null)

		@JvmField val ZERO = valueOf(0)
		@JvmField val ONE = valueOf(1)
		@JvmField val TWO = valueOf(2)
		@JvmField val TEN = valueOf(10)
		@JvmField val MINUS_ONE = valueOf(-1)
		@JvmField val MINUS_TWO = valueOf(-2)
		@JvmField val MINUS_TEN = valueOf(-10)
		@JvmField val ONE_TENTH = valueOf("0.1")
		@JvmField val MINUS_ONE_TENTH = valueOf("-0.1")

		@JvmField val INT_MAX_VALUE = Decimal(BI_INT_MAX)
		@JvmField val INT_MIN_VALUE = Decimal(BI_INT_MIN)
		@JvmField val LONG_MAX_VALUE = Decimal(BI_LONG_MAX)
		@JvmField val LONG_MIN_VALUE = Decimal(BI_LONG_MIN)

		@JvmField val FLOAT_MAX_VALUE = Decimal(BI_FLOAT_MAX)
		@JvmField val FLOAT_MIN_VALUE = Decimal(BI_FLOAT_MIN)
		@JvmField val DOUBLE_MAX_VALUE = Decimal(BI_DOUBLE_MAX)
		@JvmField val DOUBLE_MIN_VALUE = Decimal(BI_DOUBLE_MIN)
	}
}

fun FriendlyByteBuf.readDecimal() = Decimal.read(this)
fun FriendlyByteBuf.writeDecimal(value: Decimal) = value.write(this)

fun InputStream.readDecimal(sizeLimit: NbtAccounter = NbtAccounter(512L)): Decimal {
	val size = readVarIntLE(sizeLimit)
	require(size >= 0) { "Negative payload size: $size" }
	sizeLimit.accountBytes(size.toLong())
	val bytes = ByteArray(size)
	read(bytes)
	return Decimal.fromByteArray(bytes)
}

fun OutputStream.writeDecimal(value: Decimal) {
	val bytes = value.toByteArray()
	writeVarIntLE(bytes.size)
	write(bytes)
}

fun CompoundTag.getDecimal(key: String) = Decimal.deserializeNBT(this[key])
fun CompoundTag.putDecimal(key: String, value: Decimal) = put(key, value.serializeNBT())

operator fun CompoundTag.set(key: String, value: Decimal) = putDecimal(key, value)

fun Float.toDecimal() = Decimal(this)
fun Double.toDecimal() = Decimal(this)
fun Int.toDecimal() = Decimal(this)
fun Byte.toDecimal() = Decimal(this)
fun Short.toDecimal() = Decimal(this)
fun Long.toDecimal() = Decimal(this)
fun Decimal.toDecimal() = this

class DecimalConfigValue(
	parent: ForgeConfigSpec.ConfigValue<String>,
	val minimum: Decimal? = null,
	val maximum: Decimal? = null,
) : ObservedConfigValue<Decimal>(parent) {
	override fun fromString(value: String): Decimal? {
		try {
			val parsed = Decimal(value)

			if (minimum != null && minimum > parsed) {
				return minimum
			} else if (maximum != null && maximum < parsed) {
				return maximum
			}

			return parsed
		} catch (err: java.lang.NumberFormatException) {
			return null
		}
	}

	override fun toString(value: Decimal): Pair<String, Decimal> {
		if (minimum != null && minimum > value) {
			return minimum.toString() to minimum
		} else if (maximum != null && maximum < value) {
			return maximum.toString() to maximum
		}

		return value.toString() to value
	}
}

private fun ForgeConfigSpec.Builder.commentRange(minimum: Decimal?, maximum: Decimal?) {
	if (minimum != null && maximum != null) {
		comment("Range: $minimum ~ $maximum")
	} else if (minimum != null) {
		comment("Range: >= $minimum")
	} else if (maximum != null) {
		comment("Range: <= $maximum")
	}
}

fun ForgeConfigSpec.Builder.defineDecimal(path: String, defaultValue: Decimal, minimum: Decimal? = null, maximum: Decimal? = null): DecimalConfigValue {
	commentRange(minimum, maximum)
	comment("Default: $defaultValue")
	return DecimalConfigValue(define(path, defaultValue.toString()), minimum, maximum)
}

fun ForgeConfigSpec.Builder.defineDecimal(path: List<String>, defaultValue: Decimal, minimum: Decimal? = null, maximum: Decimal? = null): DecimalConfigValue {
	commentRange(minimum, maximum)
	comment("Default: $defaultValue")
	return DecimalConfigValue(define(path, defaultValue.toString()), minimum, maximum)
}

fun RandomSource.nextDecimal(min: Decimal, max: Decimal, round: Boolean = false): Decimal {
	val value = nextDouble()

	if (round) {
		return Decimal((min + (max - min) * value).whole)
	} else {
		return min + (max - min) * value
	}
}

fun RandomSource.nextVariance(value: Decimal, round: Boolean = false): Decimal {
	return nextDecimal(-value / 2, value / 2, round)
}
