package ru.dbotthepony.mc.otm.storage

import java.math.BigInteger
import java.util.*
import java.util.stream.Stream
import kotlin.Comparator

/**
 * Storage system root, along IStorageStack interface
 */
interface IStorage<T : StorageStack<T>> {
	/**
	 * @return Identity of this virtual component
	 */
	val storageType: StorageStack.Type<T>
}

/**
 * Generates events for [IStorageEventConsumer]
 */
interface IStorageEventProducer<T : StorageStack<T>> : IStorage<T> {
	/**
	 * [listener] is [IStorageEventConsumer] which want to subscribe to our events
	 */
	fun addListener(listener: IStorageEventConsumer<T>): Boolean

	/**
	 * [listener] is [IStorageEventConsumer] which want to unsubscribe from our events
	 */
	fun removeListener(listener: IStorageEventConsumer<T>): Boolean
}

/**
 * Consumes events produced by [IStorageEventConsumer]
 */
interface IStorageEventConsumer<T : StorageStack<T>> : IStorage<T> {
	/**
	 * Fired on whenever an object is added (to listener) we subscribed to
	 */
	fun onStackAdded(stack: T, id: UUID, provider: IStorageProvider<T>)

	/**
	 * Fired on whenever an object is changes on listener we subscribed to
	 */
	fun onStackChanged(stack: T, id: UUID)

	/**
	 * Fired on whenever an object is removed from listener we subscribed to
	 */
	fun onStackRemoved(id: UUID)

	fun onStackChanged(tuple: IStorageTuple<T>) {
		onStackChanged(tuple.stack, tuple.id)
	}

	fun onStackAdded(tuple: IStorageTuple<T>, provider: IStorageProvider<T>) {
		onStackAdded(tuple.stack, tuple.id, provider)
	}

	fun onStackRemoved(tuple: IStorageTuple<T>) {
		onStackRemoved(tuple.id)
	}
}

/**
 * Storage acceptor, accepts (insert only) stacks provided via [insertStack] method.
 */
interface IStorageAcceptor<T : StorageStack<T>> : IStorage<T> {
	/**
	 * Inserts an item into system.
	 * @return leftover, might equal to [stack] if no items were inserted
	 */
	fun insertStack(stack: T, simulate: Boolean): T

	val insertPriority: Int get() = 0

	companion object : Comparator<IStorageAcceptor<*>> {
		override fun compare(o1: IStorageAcceptor<*>, o2: IStorageAcceptor<*>): Int {
			return o2.insertPriority.compareTo(o1.insertPriority)
		}
	}
}

/**
 * Storage provider, provides (extract only) stacks to whoever needs them (the "warehouse").
 *
 * [get] methods work as in bidirectional map, with each stack mapping to unique [UUID]
 *
 * What this means is that [get] with [T] as an argument shall never experience a situation where
 * two stacks match provided key.
 *
 * Technical decision of using [UUID]s for primary key for locating stacks is performance.
 * The only downside is doubled memory usage for storing bidirectional map (implementation specific).
 * This allows us to avoid burden of constantly constructing keys out of stacks (e.g. [net.minecraft.world.item.ItemStack]s)
 * which may or may not produce performance hit; [UUID]s are lightweight, semantically not bound to anything and are
 * very good for distributed ID generation (so nothing in game has to be bound to one sequential number generator).
 */
interface IStorageProvider<T : StorageStack<T>> : IStorageEventProducer<T> {
	/**
	 * Attempts to retrieve stored stack by its [id].
	 *
	 * If no stack with [id] exists, then "empty stack" is returned.
	 *
	 * @param id identifier of stack
	 * @return stored object
	 */
	operator fun get(id: UUID): T

	/**
	 * Attempts to map given [key] to [UUID] inside this storage provider.
	 *
	 * Implementation requirement: There should **never** be a situation where one [key]
	 * matches two or more stacks. **Exactly one or none**.
	 *
	 * @param key stack we want to find UUID of
	 * @return UUID of stack if present
	 */
	operator fun get(key: T): UUID? {
		return stacks.filter { it.stack.equalsWithoutCount(key) }.findFirst().orElse(null)?.id
	}

	val stacks: Stream<IStorageTuple<T>>

	/**
	 * If storage has no such stack, returns empty stack
	 *
	 * @param id identifier of stack to extract
	 * @param amount amount of units to extract
	 * @param simulate whenever to simulate the action or not
	 * @return copy of object, with amount of units actually extracted
	 */
	fun extractStack(id: UUID, amount: BigInteger, simulate: Boolean): T

	fun extractStack(id: T, amount: BigInteger, simulate: Boolean): T {
		return extractStack(get(id) ?: return storageType.empty, amount, simulate)
	}

	val extractPriority: Int get() = 0

	companion object : Comparator<IStorageProvider<*>> {
		override fun compare(o1: IStorageProvider<*>, o2: IStorageProvider<*>): Int {
			return o2.extractPriority.compareTo(o1.extractPriority)
		}
	}
}

/**
 * Storage component, which basically implement Input and Output
 */
interface IStorageComponent<T : StorageStack<T>> : IStorageProvider<T>, IStorageAcceptor<T> {
	class C<T : StorageStack<T>>(private val provider: IStorageProvider<T>, private val acceptor: IStorageAcceptor<T>) : IStorageComponent<T>, IStorageProvider<T> by provider, IStorageAcceptor<T> by acceptor {
		constructor(acceptor: IStorageAcceptor<T>, provider: IStorageProvider<T>) : this(provider, acceptor)

		init {
			require(provider.storageType == acceptor.storageType) { "Provider and Accept have different storage types: ${provider.storageType} != ${acceptor.storageType}" }
		}

		override val storageType: StorageStack.Type<T>
			get() = provider.storageType

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

		override fun hashCode(): Int {
			return (provider.hashCode() * 31) xor acceptor.hashCode()
		}

		override fun toString(): String {
			return "IStorageComponent[$provider; $acceptor]"
		}
	}
}

interface IStorageTuple<T : StorageStack<T>> {
	val id: UUID
	val stack: T

	interface IMutable<T : StorageStack<T>> : IStorageTuple<T> {
		override var stack: T

		fun grow(amount: BigInteger) {
			stack = stack.grow(amount)
		}

		fun shrink(amount: BigInteger) {
			stack = stack.shrink(amount)
		}
	}

	data class I<T : StorageStack<T>>(override val id: UUID, override val stack: T) : IStorageTuple<T>
	data class M<T : StorageStack<T>>(override val id: UUID, override var stack: T) : IMutable<T>
}

/**
 * Component which (most time) proxy other components (combine their contents into single view)
 */
interface IVirtualStorageComponent<T : StorageStack<T>> : IStorageComponent<T>, IStorageEventConsumer<T> {
	fun add(identity: IStorage<T>)
	fun remove(identity: IStorage<T>)
	fun contains(identity: IStorage<T>): Boolean
}
