package ocelot.desktop.inventory

import ocelot.desktop.OcelotDesktop
import ocelot.desktop.inventory.PersistedInventory._
import ocelot.desktop.inventory.traits.{EntityItem, PersistableItem}
import ocelot.desktop.util.ReflectionUtils.findUnaryConstructor
import ocelot.desktop.util.{Logging, Persistable}
import totoro.ocelot.brain.nbt.ExtendedNBT.{extendNBTTagCompound, extendNBTTagList}
import totoro.ocelot.brain.nbt.persistence.NBTPersistence
import totoro.ocelot.brain.nbt.{NBT, NBTTagCompound}

import scala.collection.mutable

/** Provides persistence for an [[Inventory]].
  *
  * [[EntityItem]]s are treated specially: their [[EntityItem.entity entities]] are persisted
  * along with the item when saving. When loading the item, it first loads the entity and uses it to instantiate
  * the item (as required by [[EntityItem]]).
  */
trait PersistedInventory extends Inventory with Logging with Persistable {
  override type I <: Item with PersistableItem

  override def load(nbt: NBTTagCompound): Unit = {
    super.load(nbt)

    val slotsToRemove = inventoryIterator.map(_.index).to(mutable.Set)

    for (slotNbt <- nbt.getTagList(InventoryTag, NBT.TAG_COMPOUND).iterator[NBTTagCompound]) {
      val slotIndex = slotNbt.getInteger(SlotIndexTag)
      slotsToRemove -= slotIndex

      val itemNbt = slotNbt.getCompoundTag(SlotItemTag)
      val itemClass = Class.forName(slotNbt.getString(SlotItemClassTag))

      try {
        val item = if (classOf[EntityItem].isAssignableFrom(itemClass))
          loadEntityItem(itemClass, slotNbt)
        else
          loadPlainItem(itemClass)

        item.load(itemNbt)
        Slot(slotIndex).put(item)
      } catch {
        case ItemLoadException(message) =>
          logger.error(
            s"Could not load an item in the slot $slotIndex of " +
              s"the inventory $this (class ${this.getClass.getName}): $message"
          )
          onSlotLoadFailed(slotIndex)
      }
    }

    for (slotIndex <- slotsToRemove) {
      Slot(slotIndex).remove()
    }
  }

  override def save(nbt: NBTTagCompound): Unit = {
    super.save(nbt)

    nbt.setNewTagList(
      InventoryTag,
      inventoryIterator.map { slot =>
        val item = slot.get.get

        val slotNbt = new NBTTagCompound
        slotNbt.setInteger(SlotIndexTag, slot.index)

        val itemNbt = new NBTTagCompound
        item.save(itemNbt)
        slotNbt.setTag(SlotItemTag, itemNbt)
        slotNbt.setString(SlotItemClassTag, item.getClass.getName)

        item match {
          case item: EntityItem => saveEntityItem(slotNbt, item)
          case _ =>
        }

        slotNbt
      },
    )
  }

  @throws[ItemLoadException]("if the item could not be loaded")
  protected def loadEntityItem(itemClass: Class[_], slotNbt: NBTTagCompound): I = {
    val entityNbt = slotNbt.getCompoundTag(SlotEntityTag)
    val entity = NBTPersistence.load(entityNbt, OcelotDesktop.workspace)

    val constructor = findUnaryConstructor(itemClass, entity.getClass) match {
      case Some(constructor) => constructor
      case None =>
        throw ItemLoadException(
          s"an item class ${itemClass.getName} cannot be instantiated with $entity (class ${entity.getClass.getName})"
        )
    }

    constructor.newInstance(entity).asInstanceOf[I]
  }

  // noinspection ScalaWeakerAccess
  @throws[ItemLoadException]("if the item could not be loaded")
  protected def loadPlainItem(itemClass: Class[_]): I = {
    itemClass.getConstructor().newInstance().asInstanceOf[I]
  }

  protected def onSlotLoadFailed(slotIndex: Int): Unit = {
    Slot(slotIndex).removeAndDispose()
  }

  protected def saveEntityItem(slotNbt: NBTTagCompound, item: EntityItem): Unit = {
    slotNbt.setTag(SlotEntityTag, NBTPersistence.save(item.entity))
  }
}

object PersistedInventory {
  private val InventoryTag = "inventory"
  private val SlotIndexTag = "slotIndex"
  private val SlotItemTag = "item"
  private val SlotItemClassTag = "class"
  private val SlotEntityTag = "entity"

  case class ItemLoadException(message: String) extends Exception(message)
}
