package ru.dbotthepony.kstarbound.item

import com.google.gson.JsonArray
import com.google.gson.JsonElement
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntIterable
import it.unimi.dsi.fastutil.ints.IntIterator
import it.unimi.dsi.fastutil.ints.IntIterators
import it.unimi.dsi.fastutil.ints.IntList
import it.unimi.dsi.fastutil.ints.IntLists
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import java.util.random.RandomGenerator

interface IContainer : Iterable<ItemStack> {
	var size: Int
	operator fun get(index: Int): ItemStack
	operator fun set(index: Int, value: ItemStack)
	fun clear()

	fun ageItems(by: Double): Boolean {
		var any = false

		for (i in 0 until size) {
			val (newItem, hasAdvanced) = this[i].advanceAge(by)
			any = hasAdvanced || any

			if (newItem != null) {
				this[i] = newItem
			}
		}

		return any
	}

	fun hasCountOfItem(item: ItemDescriptor, exactMatch: Boolean = false): Long {
		var count = 0L

		for (i in 0 until size) {
			if (this[i].matches(item, exactMatch)) {
				count += this[i].size
			}
		}

		return count
	}

	fun hasCountOfItem(item: ItemStack, exactMatch: Boolean = false): Long {
		var count = 0L

		for (i in 0 until size) {
			if (this[i].matches(item, exactMatch)) {
				count += this[i].size
			}
		}

		return count
	}

	fun shuffle(random: RandomGenerator, split: Boolean = true, splitChance: Float = 0.8f) {
		for (i in 0 until size) {
			val rand = random.nextInt(size)
			val a = this[i]
			val b = this[rand]

			this[rand] = a
			this[i] = b
		}

		if (split) {
			val emptySlots = IntArrayList()
			val splittableSlots = IntArrayList()

			for (i in 0 until size) {
				if (this[i].isEmpty) {
					emptySlots.add(i)
				} else if (this[i].size > 1L) {
					splittableSlots.add(i)
				}
			}

			if (splittableSlots.isNotEmpty() && emptySlots.isNotEmpty()) {
				for (tslot in splittableSlots) {
					if (emptySlots.isEmpty)
						break

					if (random.nextFloat() <= splitChance) {
						var currentChance = splitChance
						val workingSlots = IntArrayList()
						workingSlots.add(tslot)

						while (workingSlots.isNotEmpty() && emptySlots.isNotEmpty()) {
							val slot = workingSlots.removeInt(0)
							val item = this[slot]
							val splitAmount = random.nextLong(1L, item.size)
							val copy = item.copy(splitAmount)
							item.size -= splitAmount

							val targetSlot = emptySlots.removeInt(random.nextInt(emptySlots.size))
							this[targetSlot] = copy

							currentChance *= 0.75f

							if (item.size > 1L && random.nextFloat() <= currentChance) {
								workingSlots.add(slot)
							}

							if (splitAmount > 1L && random.nextFloat() <= currentChance) {
								workingSlots.add(targetSlot)
							}
						}
					}
				}
			}
		}
	}

	/**
	 * not inserted items -> slots touched
	 */
	fun add(item: ItemStack, simulate: Boolean = false): Pair<ItemStack, IntList> {
		return put({ IntIterators.fromTo(0, size) }, item, simulate)
	}

	/**
	 * not inserted items -> slots touched
	 */
	fun stackWithExisting(item: ItemStack, simulate: Boolean = false): Pair<ItemStack, IntList> {
		val validSlots = IntArrayList(size)

		for (slot in 0 until size) {
			if (this[slot].isNotEmpty) {
				validSlots.add(slot)
			}
		}

		return put(validSlots, item, simulate)
	}

	/**
	 * not inserted items -> slots touched
	 */
	fun put(slots: IntIterable, item: ItemStack, simulate: Boolean = false): Pair<ItemStack, IntList> {
		return put(slots::iterator, item, simulate)
	}

	/**
	 * not inserted items -> slots touched
	 */
	fun put(slots: () -> IntIterator, item: ItemStack, simulate: Boolean = false): Pair<ItemStack, IntList> {
		val copy = item.copy()
		val touched = IntArrayList()
		var itr = slots.invoke()

		while (itr.hasNext()) {
			val slot = itr.nextInt()

			if (slot !in 0 until size)
				continue

			val itemThere = this[slot]

			if (itemThere.isStackable(copy)) {
				touched.add(slot)
				val newCount = (itemThere.size + copy.size).coerceAtMost(itemThere.maxStackSize)
				val diff = newCount - itemThere.size

				copy.size -= diff

				if (!simulate)
					itemThere.size += diff

				if (copy.isEmpty)
					return ItemStack.EMPTY to touched
			}
		}

		itr = slots.invoke()

		while (itr.hasNext()) {
			val slot = itr.nextInt()

			if (slot !in 0 until size)
				continue

			val itemThere = this[slot]

			if (itemThere.isEmpty) {
				touched.add(slot)

				if (copy.size > copy.maxStackSize) {
					if (!simulate)
						this[slot] = copy.copy(copy.maxStackSize)

					copy.size -= copy.maxStackSize
				} else {
					if (!simulate)
						this[slot] = copy

					return ItemStack.EMPTY to touched
				}
			}
		}

		return copy to touched
	}

	fun take(slot: Int, amount: Long): ItemStack {
		if (slot !in 0 until size || amount <= 0L)
			return ItemStack.EMPTY

		val item = this[slot]

		if (item.isEmpty)
			return item

		if (item.size > amount) {
			val copy = item.copy(amount)
			item.shrink(amount)
			return copy
		} else {
			this[slot] = ItemStack.EMPTY
			return item
		}
	}

	fun takeExact(slot: Int, amount: Long): Boolean {
		if (amount <= 0L)
			return true

		if (slot !in 0 until size)
			return false

		val item = this[slot]

		if (item.isEmpty)
			return false

		if (item.size > amount) {
			item.shrink(amount)
			return true
		} else {
			return false
		}
	}

	fun swap(slot: Int, item: ItemStack, tryCombine: Boolean = true): ItemStack {
		val existingItem = this[slot]

		if (item.isEmpty && existingItem.isEmpty) {
			return ItemStack.EMPTY
		} else if (item.isEmpty && existingItem.isNotEmpty) {
			// If we are passed in nothing, simply return what's there, if anything.
			this[slot] = ItemStack.EMPTY
			return existingItem
		} else if (item.isNotEmpty && existingItem.isEmpty) {
			// place into slot
			// use put because item might be bigger than max stack size
			return put(IntLists.singleton(slot), item).first
		} else {
			// If something is there, try to stack with it first.  If we can't stack,
			// then swap.

			if (tryCombine && existingItem.isStackable(item)) {
				return put(IntLists.singleton(slot), item).first
			} else {
				this[slot] = ItemStack.EMPTY
				val slots = IntArrayList(IntIterators.fromTo(0, size))
				slots.removeInt(slot)
				slots.add(0, slot)

				val remaining = put(slots, item).first

				if (remaining.isNotEmpty && this[slot].isStackable(remaining)) {
					// damn
					this[slot].grow(remaining.size)
				}

				return existingItem
			}
		}
	}

	fun combineAt(slot: Int, item: ItemStack): ItemStack {
		if (this[slot].isEmpty || !this[slot].isStackable(item)) {
			return item
		} else {
			return put(IntLists.singleton(slot), item).first
		}
	}

	fun take(descriptor: ItemDescriptor, exactMatch: Boolean = false, simulate: Boolean = false): Boolean {
		var toTake = descriptor.count

		if (toTake <= 0L)
			return true

		val extractSlots = IntArrayList()

		for (slot in 0 until size) {
			val item = this[slot]

			if (item.matches(descriptor, exactMatch)) {
				toTake -= item.size
				if (!simulate) extractSlots.add(slot)
				if (toTake <= 0L) break
			}
		}

		if (simulate || toTake > 0L)
			return toTake <= 0L

		toTake = descriptor.count

		for (slot in extractSlots) {
			if (this[slot].size <= toTake) {
				toTake -= this[slot].size
				this[slot] = ItemStack.EMPTY
			} else {
				this[slot].shrink(toTake)
				break
			}
		}

		return true
	}

	fun take(descriptor: ItemStack, exactMatch: Boolean = false, simulate: Boolean = false): Boolean {
		var toTake = descriptor.size

		if (toTake <= 0L)
			return true

		val extractSlots = IntArrayList()

		for (slot in 0 until size) {
			val item = this[slot]

			if (item.matches(descriptor, exactMatch)) {
				toTake -= item.size
				if (!simulate) extractSlots.add(slot)
				if (toTake <= 0L) break
			}
		}

		if (simulate || toTake > 0L)
			return toTake <= 0L

		toTake = descriptor.size

		for (slot in extractSlots) {
			if (this[slot].size <= toTake) {
				toTake -= this[slot].size
				this[slot] = ItemStack.EMPTY
			} else {
				this[slot].shrink(toTake)
				break
			}
		}

		return true
	}

	fun toJson(store: Boolean = false): JsonArray {
		val result = JsonArray(size)

		if (store) {
			Starbound.storeJson {
				for (i in 0 until size) {
					result.add(this[i].toJson())
				}
			}
		} else {
			for (i in 0 until size) {
				result.add(this[i].toJson())
			}
		}

		return result
	}

	fun fromJson(json: JsonElement, resize: Boolean = false): List<ItemStack> {
		clear()

		val read = ArrayList<ItemStack>()
		val array = json.asJsonArray
		var nonEmpty = 0

		for (i in 0 until array.size()) {
			val item = ItemDescriptor(array[i])

			if (item.isNotEmpty)
				nonEmpty++

			read.add(item.build())
		}

		if (read.size > size) {
			// we have a problem

			if (resize) {
				// no problem
				size = read.size

				for ((i, item) in read.withIndex()) {
					this[i] = item
				}

				return emptyList()
			} else {
				// first, put items to their place if they are within container size

				for (i in 0 until size) {
					this[i] = read[i]
				}

				// second, try to move remaining item into empty or stackable slots
				val lost = ArrayList<ItemStack>()

				for (i in size until read.size) {
					val remaining = add(read[i]).first

					if (remaining.isNotEmpty) {
						lost.add(remaining)
					}
				}

				// if we weren't capable of putting excess items into container, then what a shame.
				return lost
			}
		} else {
			if (read.size < size && resize)
				size = read.size

			for ((i, item) in read.withIndex()) {
				this[i] = item
			}

			return emptyList()
		}
	}

	override fun iterator(): Iterator<ItemStack> {
		return ContainerIterator(this)
	}
}
