package ru.dbotthepony.mc.otm.storage

import it.unimi.dsi.fastutil.objects.ObjectArraySet
import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage
import ru.dbotthepony.mc.otm.core.math.isPositive
import ru.dbotthepony.mc.otm.core.math.isZero
import java.math.BigInteger
import java.util.*
import java.util.stream.Stream
import kotlin.collections.HashMap
import kotlin.reflect.full.isSubclassOf

class RemoteTuple<T : IStorageStack>(
	override val stack: T,
	override val id: UUID,
	val provider: IStorageProvider<T>,
	val local: LocalTuple<T>
) : IStorageTuple<T> {
	fun extract(amount: BigInteger, simulate: Boolean): T {
		return provider.extractStack(id, amount, simulate)
	}

	override fun equals(other: Any?): Boolean {
		return other is RemoteTuple<*> && other.id == id
	}

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

class LocalTuple<T : IStorageStack>(override val stack: T, override val id: UUID, val tuples: ArrayList<RemoteTuple<T>>) : IStorageTuple<T>

open class VirtualComponent<T : IStorageStack>(type: StorageStackType<T>) : IVirtualStorageComponent<T> {
	constructor(type: Class<T>) : this(StorageRegistry.get(type))

	override val storageType: StorageStackType<T> = type

	// удаленный UUID -> Кортеж
	protected val remoteTuples = HashMap<UUID, RemoteTuple<T>>()

	// локальный UUID -> Локальный кортеж
	protected val localTuples = HashMap<UUID, LocalTuple<T>>()

	// Стак -> Локальный кортеж стака
	protected val hashedTuples = HashMap<T, LocalTuple<T>>()

	// ArrayList для скорости работы
	protected val listeners: MutableSet<IStorageEventConsumer<T>> = ObjectArraySet()
	protected val children: MutableSet<IStorage<T>> = ObjectArraySet()
	protected val consumers: MutableSet<IStorageAcceptor<T>> = ObjectArraySet()

	protected open fun onAdd(identity: IStorage<T>) {}
	protected open fun onRemove(identity: IStorage<T>) {}

	override fun add(identity: IStorage<T>) {
		if (!(identity.storageType::class.isSubclassOf(storageType::class)))
			throw ClassCastException("Reified Generics: ${identity.storageType::class.qualifiedName} can not be cast into ${storageType::class.qualifiedName}")

		if (children.add(identity)) {
			if (identity is IStorageProvider<T>) {
				identity.addListenerAuto(this)
			} else if (identity is IStorageEventProducer<T>) {
				identity.addListener(this)
			}

			if (identity is IStorageEventConsumer<T>) {
				addListenerAuto(identity)
			}

			if (identity is IStorageAcceptor<T>) {
				consumers.add(identity)
			}

			onAdd(identity)
		}
	}

	override fun remove(identity: IStorage<T>) {
		if (!(identity.storageType::class.isSubclassOf(storageType::class)))
			throw ClassCastException("Reified Generics: ${identity.storageType::class.qualifiedName} can not be cast into ${storageType::class.qualifiedName}")

		if (children.remove(identity)) {
			if (identity is IStorageProvider<T>) {
				identity.removeListenerAuto(this)
			} else if (identity is IStorageEventProducer<T>) {
				identity.removeListener(this)
			}

			if (identity is IStorageEventConsumer<T>) {
				removeListenerAuto(identity)
			}

			if (identity is IStorageAcceptor<T>) {
				consumers.remove(identity)
			}

			onRemove(identity)
		}
	}

	override fun contains(identity: IStorage<T>): Boolean {
		return children.contains(identity)
	}

	override fun addListener(listener: IStorageEventConsumer<T>): Boolean {
		if (listeners.add(listener)) {
			return true
		}

		return false
	}

	override fun removeListener(listener: IStorageEventConsumer<T>): Boolean {
		return listeners.remove(listener)
	}

	override fun get(id: UUID): T {
		return localTuples[id]?.stack ?: this.storageType.empty
	}

	override val stacks: Stream<IStorageTuple<T>> get() {
		return ArrayList<IStorageTuple<T>>(hashedTuples.size).also { it.addAll(hashedTuples.values) }.stream()
	}

	override fun get(key: T): UUID? {
		return hashedTuples[key.key()]?.id
	}

	@Suppress("unchecked_cast")
	override fun addStack(stack: T, id: UUID, provider: IStorageProvider<T>) {
		check(!remoteTuples.containsKey(id)) { "Already tracking tuple with id $id" }

		val key = stack.key()
		var local: LocalTuple<T>? = hashedTuples[key]
		var oldCount = BigInteger.ZERO
		val added = local == null

		if (local == null) {
			local = LocalTuple(stack.copy() as T, UUID.randomUUID(), ArrayList<RemoteTuple<T>>(1))
			localTuples[local.id] = local
			hashedTuples[key] = local
		} else {
			oldCount = local.stack.count
			local.stack.grow(stack.count)
		}

		val remote = RemoteTuple(stack.copy() as T, id, provider, local)
		local.tuples.add(remote)
		remoteTuples[id] = remote

		if (added) {
			for (listener in listeners) {
				listener.addStack(local.stack, local.id, this)
			}
		} else {
			for (listener in listeners) {
				listener.changeStack(local.stack, local.id, oldCount)
			}
		}
	}

	override fun changeStack(stack: T, id: UUID, oldCount: BigInteger) {
		require(stack.count.isPositive)
		val tuple = remoteTuples[id] ?: throw IllegalStateException("No such tuple with id $id")

		val diff = stack.count - tuple.stack.count
		tuple.stack.count = stack.count

		@Suppress("NAME_SHADOWING") val oldCount = tuple.local.stack.count
		tuple.local.stack.grow(diff)

		for (listener in listeners) {
			listener.changeStack(tuple.local.stack, tuple.local.id, oldCount)
		}
	}

	override fun removeStack(stack: T, id: UUID) {
		val tuple = remoteTuples[id] ?: throw IllegalStateException("No such tuple with id $id")

		tuple.local.stack.shrink(tuple.stack.count)
		tuple.local.tuples.remove(tuple)

		remoteTuples.remove(id)

		val a = tuple.local.stack.count <= BigInteger.ZERO
		val b = tuple.local.tuples.size == 0

		if (a || b) {
			check(a && b) { "View object is empty, but tuple list is not!" }
			localTuples.remove(tuple.local.id)
			val key = stack.key()
			checkNotNull(hashedTuples.remove(key)) { "No such stack $key" }

			for (listener in listeners) {
				listener.removeStack(tuple.local.stack, tuple.local.id)
			}
		}
	}

	override fun insertStack(stack: T, simulate: Boolean): T {
		var leftover = stack

		for (consumer in consumers) {
			leftover = consumer.insertStack(leftover, simulate)

			if (leftover.isEmpty) {
				return leftover
			}
		}

		return leftover
	}

	@Suppress("unchecked_cast")
	override fun extractStack(id: UUID, amount: BigInteger, simulate: Boolean): T {
		if (!amount.isPositive)
			return this.storageType.empty

		@Suppress("name_shadowing")
		var amount = amount
		val tuple: LocalTuple<T>? = localTuples[id]

		if (tuple == null || amount.isZero)
			return this.storageType.empty

		if (!amount.isPositive)
			amount = tuple.stack.maxStackSize ?: tuple.stack.count

		val toExtract = tuple.stack.count.coerceAtMost(amount)
		var extracted = BigInteger.ZERO
		val copy = tuple.stack.copy() as T

		for (remote_tuple in tuple.tuples.let { Array(it.size) { i -> it[i] } }) {
			val extractedStack = remote_tuple.extract(toExtract - extracted, simulate)
			extracted += extractedStack.count

			if (extracted >= toExtract)
				break
		}

		if (extracted.isPositive) {
			copy.count = extracted
			return copy
		}

		return this.storageType.empty
	}
}

/**
 * Adds energy demand to operations over [parent]
 */
open class PoweredComponent<T : IStorageStack>(open val parent: IStorageComponent<T>, val energyProvider: () -> IMatteryEnergyStorage) : IStorageComponent<T> {
	override val storageType: StorageStackType<T>
		get() = parent.storageType

	private val subscribedThrough = ObjectArraySet<IStorageEventConsumer<T>>()

	final override fun addListener(listener: IStorageEventConsumer<T>): Boolean {
		if (parent.addListener(listener)) {
			subscribedThrough.add(listener)
			return true
		}

		return false
	}

	final override fun removeListener(listener: IStorageEventConsumer<T>): Boolean {
		subscribedThrough.remove(listener)
		return parent.removeListener(listener)
	}

	fun removeListeners() {
		for (child in subscribedThrough) {
			parent.removeListener(child)
		}

		subscribedThrough.clear()
	}

	override fun get(key: T) = parent[key]

	@Suppress("unchecked_cast")
	override fun insertStack(stack: T, simulate: Boolean): T {
		val required = storageType.energyPerOperation * stack.count
		val energy = energyProvider.invoke()
		val extracted = energy.extractEnergy(required, true)

		if (extracted.isZero) {
			return stack.copy() as T
		}

		if (extracted == required) {
			val leftover = parent.insertStack(stack, simulate)

			if (leftover.isEmpty) {
				if (!simulate) {
					energy.extractEnergy(required, false)
				}

				return leftover
			}

			if (!simulate) {
				val requiredNew = storageType.energyPerOperation * (stack.count - leftover.count)
				energy.extractEnergy(requiredNew, false)
			}

			return leftover
		}

		@Suppress("name_shadowing")
		val stack = stack.copy() as T
		val oldCount = stack.count
		stack.count = (extracted / storageType.energyPerOperation).whole
		val diff = oldCount - stack.count
		val newRequired = storageType.energyPerOperation * stack.count
		val newExtracted = energy.extractEnergy(newRequired, true)

		if (newExtracted == newRequired) {
			val leftover = parent.insertStack(stack, simulate)

			if (leftover.isEmpty) {
				if (!simulate) {
					energy.extractEnergy(newRequired, false)
				}

				leftover.count = diff
				return leftover
			}

			if (!simulate) {
				val requiredNew = storageType.energyPerOperation * (stack.count - leftover.count)
				energy.extractEnergy(requiredNew, false)
			}

			leftover.count += diff
			return leftover
		}

		return stack
	}

	override fun get(id: UUID) = parent[id]

	override fun extractStack(id: UUID, amount: BigInteger, simulate: Boolean): T {
		val required = storageType.energyPerOperation * amount
		val energy = energyProvider.invoke()
		val extracted = energy.extractEnergy(required, true)

		if (extracted.isZero) {
			return storageType.empty
		}

		if (extracted == required) {
			val extractedStack = parent.extractStack(id, amount, simulate)

			if (extractedStack.isEmpty) {
				return extractedStack
			}

			if (!simulate) {
				if (extractedStack.count == amount) {
					energy.extractEnergy(required, false)
				} else {
					energy.extractEnergy(storageType.energyPerOperation * extractedStack.count, false)
				}
			}

			return extractedStack
		}

		@Suppress("name_shadowing")
		val amount = (required / storageType.energyPerOperation).whole
		val extractedStack = parent.extractStack(id, amount, simulate)

		if (extractedStack.isEmpty) {
			return extractedStack
		}

		if (!simulate) {
			energy.extractEnergy(storageType.energyPerOperation * extractedStack.count, false)
		}

		return extractedStack
	}

	override val stacks get() = parent.stacks
}

/**
 * Adds energy demand to virtual component [parent]
 */
open class PoweredVirtualComponent<T : IStorageStack>(override val parent: IVirtualStorageComponent<T>, energyProvider: () -> IMatteryEnergyStorage)
	: PoweredComponent<T>(parent, energyProvider), IVirtualStorageComponent<T> {
	constructor(parent: IVirtualStorageComponent<T>, energy: IMatteryEnergyStorage) : this(parent, { energy })
	constructor(parent: Class<T>, energy: IMatteryEnergyStorage) : this(VirtualComponent(parent), { energy })
	constructor(parent: StorageStackType<T>, energy: IMatteryEnergyStorage) : this(VirtualComponent(parent), { energy })

	override fun addStack(stack: T, id: UUID, provider: IStorageProvider<T>) = parent.addStack(stack, id, provider)
	override fun changeStack(stack: T, id: UUID, oldCount: BigInteger) = parent.changeStack(stack, id, oldCount)
	override fun removeStack(stack: T, id: UUID) = parent.removeStack(stack, id)

	override fun add(identity: IStorage<T>) = parent.add(identity)
	override fun remove(identity: IStorage<T>) = parent.remove(identity)
	override fun contains(identity: IStorage<T>) = parent.contains(identity)
}
