package ru.dbotthepony.kstarbound.item

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.classdump.luna.Table
import org.classdump.luna.TableFactory
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeVarLong
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.Drawable
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.item.ItemRarity
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.ManualLazy
import ru.dbotthepony.kstarbound.util.valueOf
import java.io.DataOutputStream
import java.lang.Math.toRadians
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.max
import kotlin.properties.Delegates

/**
 * Base class for instanced items in game
 *
 * [config] is JsonObject returned by "builder" Lua script, or [entry]'s json if no such script exists
 * or it has failed
 */
@JsonAdapter(ItemStack.Adapter::class)
open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, parameters: JsonObject, size: Long) {
	/**
	 * unique number utilized to determine whenever stack has changed
	 */
	var changeset: Long = CHANGESET.incrementAndGet()
		private set

	/**
	 * it uses global atomic long to guarantee stacks having different
	 * changesets throughout entire lifetime of game
	 */
	protected fun bumpVersion() {
		changeset = CHANGESET.incrementAndGet()
	}

	var size: Long = size
		set(value) {
			val newValue = value.coerceAtLeast(0L)

			if (field != newValue) {
				field = newValue
				changeset = CHANGESET.incrementAndGet()
			}
		}

	var parameters: JsonObject = parameters
		protected set

	protected val parametersLazies = ObjectArrayList<ManualLazy<*>>() // no CME checks

	protected val mergedJson = ManualLazy {
		mergeJson(config.deepCopy(), parameters)
	}.also { parametersLazies.add(it) }

	val isEmpty: Boolean
		get() = size <= 0 || entry.isEmpty

	val isNotEmpty: Boolean
		get() = size > 0 && !entry.isEmpty

	fun grow(amount: Long) {
		size += amount
	}

	fun shrink(amount: Long) {
		size -= amount
	}

	protected fun lookupProperty(name: String): JsonElement {
		return mergedJson.value[name] ?: JsonNull.INSTANCE
	}

	protected fun lookupProperty(name: String, orElse: JsonElement): JsonElement {
		return mergedJson.value[name] ?: orElse
	}

	protected fun lookupProperty(name: String, orElse: () -> JsonElement): JsonElement {
		return mergedJson.value[name] ?: orElse()
	}

	protected fun <T> lazyProperty(name: String, default: JsonElement, transform: JsonElement.() -> T): ManualLazy<T> {
		return ManualLazy {
			transform(lookupProperty(name, default))
		}.also { parametersLazies.add(it) }
	}

	protected fun <T> lazyProperty(name: String, transform: JsonElement.() -> T): ManualLazy<T> {
		return ManualLazy {
			transform(lookupProperty(name))
		}.also { parametersLazies.add(it) }
	}

	protected fun <T> lazyProperty(name: String, default: () -> JsonElement, transform: JsonElement.() -> T): ManualLazy<T> {
		return ManualLazy {
			transform(lookupProperty(name, default))
		}.also { parametersLazies.add(it) }
	}

	val maxStackSize: Long by lazyProperty("maxStack", JsonPrimitive(Globals.itemParameters.defaultMaxStack)) { asLong }
	val shortDescription: String by lazyProperty("shortdescription", JsonPrimitive("")) { asString }
	val description: String by lazyProperty("description", JsonPrimitive("")) { asString }
	val rarity: ItemRarity by lazyProperty("rarity") { ItemRarity.entries.valueOf(asString) }
	val twoHanded: Boolean by lazyProperty("twoHanded", JsonPrimitive(false)) { asBoolean }
	val price: Long by lazyProperty("price", JsonPrimitive(Globals.itemParameters.defaultPrice)) { asLong }
	val tooltipKind: String by lazyProperty("tooltipKind", JsonPrimitive("")) { asString }
	val category: String by lazyProperty("category", JsonPrimitive("")) { asString }
	val pickupSounds: Set<String> by lazyProperty("pickupSounds", { JsonArray() }) { if (asJsonArray.isEmpty) Globals.itemParameters.pickupSoundsAbsolute else asJsonArray.stream().map { it.asString }.collect(ImmutableSet.toImmutableSet()) }
	val timeToLive: Double by lazyProperty("timeToLive", JsonPrimitive(Globals.itemParameters.defaultTimeToLive)) { asDouble }
	val learnBlueprintsOnPickup: Set<ItemDescriptor> by lazyProperty("learnBlueprintsOnPickup", { JsonArray() }) { asJsonArray.stream().map { ItemDescriptor(it) }.collect(ImmutableSet.toImmutableSet()) }

	// collection -> entry
	val collectablesOnPickup: Map<String, String> by lazyProperty("collectablesOnPickup", { JsonObject() }) { asJsonObject.entrySet().stream().map { it.key to it.value.asString }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) }

	var drawables: ImmutableList<Drawable> by Delegates.notNull()
		private set

	protected fun setIconDrawables(drawables: Collection<Drawable>) {
		val drawablesList = ArrayList(drawables)

		if (drawablesList.isNotEmpty()) {
			var boundingBox = drawablesList.first().boundingBox(true)

			for (i in 1 until drawablesList.size) {
				boundingBox = boundingBox.combine(drawablesList[i].boundingBox(true))
			}

			for (drawable in drawablesList.indices) {
				drawablesList[drawable] = drawablesList[drawable].translate(boundingBox.centre.toFloatVector())
			}

			// as original engine puts it:
			// TODO: Why 16?  Is this the size of the icon container? Shouldn't this be configurable?

			// in other news,
			// TODO: scaling is always centered on 0.0, 0.0; this also should be configurable.

			val zoom = 16.0 / max(boundingBox.width, boundingBox.height)

			if (zoom < 1.0) {
				for (drawable in drawablesList.indices) {
					drawablesList[drawable] = drawablesList[drawable].scale(zoom.toFloat())
				}
			}
		}

		this.drawables = ImmutableList.copyOf(drawablesList)
	}

	init {
		val inventoryIcon = lookupProperty("inventoryIcon", JsonPrimitive(Globals.itemParameters.missingIcon.fullPath))

		if (inventoryIcon.isJsonArray) {
			val drawables = ArrayList<Drawable>()

			for (drawable in inventoryIcon.asJsonArray) {
				drawable as JsonObject

				val image = SpriteReference.create(AssetPathStack.relativeTo(entry.directory, drawable["image"].asString))
				val position: Vector2f

				if ("position" in drawable) {
					position = Starbound.gson.fromJson(drawable["position"], Vector2f::class.java)
				} else {
					position = Vector2f.ZERO
				}

				val scale = if ("scale" in drawable) {
					scales.fromJsonTree(drawable["scale"])
				} else {
					Either.left(1f)
				}

				val color = if ("color" in drawable) {
					colors.fromJsonTree(drawable["color"])
				} else {
					RGBAColor.WHITE
				}

				val rotation = toRadians(drawable.get("rotation", 0.0)).toFloat()
				val transforms = Drawable.Transformations(drawable.get("centered", true), rotation, drawable.get("mirrored", false), scale)

				drawables.add(Drawable.Image(image, Either.right(transforms), position, color, drawable.get("fullbright", false)))
			}

			setIconDrawables(drawables)
		} else {
			val image = SpriteReference.create(AssetPathStack.relativeTo(entry.directory, inventoryIcon.asString))
			val transforms = Drawable.CENTERED
			val drawable = Drawable.Image(image, Either.right(transforms))
			setIconDrawables(listOf(drawable))
		}
	}

	data class AgingResult(val new: ItemStack?, val ageUpdated: Boolean)

	private val agingScripts: LuaEnvironment? by lazy {
		//val config = config.value ?: return@lazy null
		//if (config.itemTags)
		null
	}

	open fun advanceAge(by: Double): AgingResult {
		val agingScripts = agingScripts ?: return AgingResult(null, false)

		val descriptor = createDescriptor()
		val updated = ItemDescriptor(agingScripts.invokeGlobal("ageItem", descriptor.toTable(agingScripts), by)[0] as Table)

		if (descriptor != updated) {
			if (descriptor.name == updated.name) {
				// only parameters got changed
				this.parameters = descriptor.parameters
				this.size = descriptor.count
				changeset = CHANGESET.incrementAndGet()
				return AgingResult(null, true)
			} else {
				// item got replaced by something else
				return AgingResult(updated.build(), true)
			}
		}

		return AgingResult(null, false)
	}

	fun createDescriptor(): ItemDescriptor {
		if (isEmpty)
			return ItemDescriptor.EMPTY

		return ItemDescriptor(entry.name, size, parameters.deepCopy())
	}

	// faster than creating an item descriptor and writing it (because it avoids copying and allocation)
	fun write(stream: DataOutputStream) {
		if (isEmpty) {
			stream.writeBinaryString("")
			stream.writeVarLong(0L)
			stream.writeJsonElement(JsonNull.INSTANCE)
		} else {
			stream.writeBinaryString(entry.name)
			stream.writeVarLong(size)
			stream.writeJsonElement(parameters)
		}
	}

	/**
	 * Возвращает null если этот предмет пуст
	 */
	fun conciseToNull(): ItemStack? {
		if (isEmpty) {
			return null
		} else {
			return this
		}
	}

	fun mergeFrom(other: ItemStack, simulate: Boolean) {
		if (isStackable(other)) {
			val newCount = (size + other.size).coerceAtMost(maxStackSize)
			val diff = newCount - size
			other.size -= diff

			if (!simulate)
				size = newCount
		}
	}

	fun isStackable(other: ItemStack): Boolean {
		if (isEmpty || other.isEmpty)
			return false

		return size != 0L && other.size != 0L && maxStackSize > size && other.config == config && other.parameters == parameters
	}

	override fun equals(other: Any?): Boolean {
		if (other !is ItemStack)
			return false

		if (isEmpty)
			return other.isEmpty

		return other.size == size && other.config == config && other.parameters == parameters
	}

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

	override fun toString(): String {
		if (isEmpty)
			return "ItemStack.EMPTY"

		return "ItemStack[${entry.name}, count = $size, params = $parameters]"
	}

	open fun copy(size: Long = this.size): ItemStack {
		if (isEmpty)
			return EMPTY

		return ItemStack(entry, config, parameters.deepCopy(), size)
	}

	fun toJson(): JsonObject? {
		if (isEmpty)
			return null

		return JsonObject().also {
			it.add("name", JsonPrimitive(entry.name))
			it.add("count", JsonPrimitive(size))
			it.add("parameters", parameters.deepCopy())
		}
	}

	fun toTable(allocator: TableFactory): Table? {
		if (isEmpty) {
			return null
		}

		return allocator.newTable(0, 3).also {
			it.rawset("name", entry.name)
			it.rawset("count", size)
			it.rawset("parameters", allocator.from(parameters))
		}
	}

	class Adapter(gson: Gson) : TypeAdapter<ItemStack>() {
		override fun write(out: JsonWriter, value: ItemStack?) {
			val json = value?.toJson()

			if (json == null)
				out.nullValue()
			else
				out.value(json)
		}

		override fun read(`in`: JsonReader): ItemStack {
			if (`in`.consumeNull())
				return EMPTY

			return ItemDescriptor(Starbound.ELEMENTS_ADAPTER.read(`in`)).build()
		}
	}

	companion object {
		private val CHANGESET = AtomicLong()

		@JvmField
		val EMPTY = ItemStack(ItemRegistry.AIR, ItemRegistry.AIR.json, JsonObject(), 0L)

		private val vectors by lazy {
			Starbound.gson.getAdapter(object : TypeToken<Vector2f>() {})
		}

		private val scales by lazy {
			Starbound.gson.getAdapter(object : TypeToken<Either<Float, Vector2f>>() {})
		}

		private val colors by lazy {
			Starbound.gson.getAdapter(object : TypeToken<RGBAColor>() {})
		}
	}
}
