package ocelot.desktop.inventory

import ocelot.desktop.OcelotDesktop
import ocelot.desktop.inventory.PersistedInventory.ItemLoadException
import ocelot.desktop.inventory.SyncedInventory.SlotStatus.SlotStatus
import ocelot.desktop.inventory.SyncedInventory.SyncDirection.SyncDirection
import ocelot.desktop.inventory.SyncedInventory._
import ocelot.desktop.inventory.traits.EntityItem
import ocelot.desktop.ui.event.{BrainEvent, EventAware}
import ocelot.desktop.util.Logging
import ocelot.desktop.util.ReflectionUtils.findUnaryConstructor
import totoro.ocelot.brain.entity.traits.{Entity, Environment, Inventory => BrainInventory}
import totoro.ocelot.brain.event.{InventoryEntityAddedEvent, InventoryEntityRemovedEvent}
import totoro.ocelot.brain.nbt.NBTTagCompound

import java.util.UUID
import scala.annotation.tailrec

/** A [[PersistedInventory]] backed by a [[BrainInventory brain Inventory]].
  *
  * Synchronizes the contents of the two inventories, propagating changes from one to the other.
  *
  * When a new [[Item]] is added to the Desktop inventory, its [[EntityItem.entity]] is added to
  * the [[brainInventory]]. When a new [[Entity]] is added to the [[brainInventory]], an [[Item]] is recovered from it
  * by using an appropriate [[ItemRecoverer]] from [[Items the registry]].
  *
  * While synchronizing, relies on the convergence of changes, but guards against stack overflows
  * by limiting the recursion depth.
  */
trait SyncedInventory extends PersistedInventory with EventAware with Logging {
  override type I <: EntityItem

  // to avoid synchronization while we're loading stuff
  private var isLoading = false

  @volatile
  private var syncFuel: Int = _

  refuel()

  /** The backing [[BrainInventory brain Inventory]]. */
  def brainInventory: BrainInventory

  override def load(nbt: NBTTagCompound): Unit = {
    isLoading = true

    try {
      super.load(nbt)
    } finally {
      isLoading = false
    }

    syncSlots()
  }

  @throws[ItemLoadException]("if the item could not be loaded")
  override protected def loadEntityItem(itemClass: Class[_], slotNbt: NBTTagCompound): I = {
    trait Matcher {
      def matches(entity: Entity): Boolean
    }

    val matcher = if (slotNbt.hasKey(SlotEntityIdTag)) {
      new Matcher {
        private val entityId = UUID.fromString(slotNbt.getString(SlotEntityIdTag))

        override def toString: String = s"entity with id `$entityId`"

        override def matches(entity: Entity): Boolean = entity.entityId == entityId
      }
    } else {
      // migration code for older saves, which relied on entity addresses.
      new Matcher {
        private val entityAddress = slotNbt.getString(SlotEntityAddressTag)

        override def toString: String = s"entity with address `$entityAddress"

        override def matches(entity: Entity): Boolean = entity match {
          // why not OcelotDesktop.workspace.entityByAddress?
          // well, that one only looks for tile entities (computers, screens, etc.),
          // and our items are none of that...
          case env: Environment => env.node.address == entityAddress
          case _ => false
        }
      }
    }

    val entity = brainInventory.inventory.iterator
      .flatMap(_.get)
      .find(matcher.matches)
      .getOrElse(throw ItemLoadException(s"$matcher has disappeared"))

    findUnaryConstructor(itemClass, entity.getClass) match {
      case Some(constructor) => constructor.newInstance(entity).asInstanceOf[I]

      case None =>
        throw ItemLoadException(
          s"an item class ${itemClass.getName} cannot be instantiated " +
            s"with $entity (class ${entity.getClass.getName})"
        )
    }
  }

  override protected def onSlotLoadFailed(slotIndex: Int): Unit = {
    // we'll deal with it the during synchronization
  }

  override protected def saveEntityItem(slotNbt: NBTTagCompound, item: EntityItem): Unit = {
    slotNbt.setString(SlotEntityIdTag, item.entity.entityId.toString)
  }

  eventHandlers += {
    case BrainEvent(InventoryEntityAddedEvent(slot, _)) if slot.inventory.owner eq brainInventory =>
      sync(slot.index, SyncDirection.BrainToDesktop)

    case BrainEvent(InventoryEntityRemovedEvent(slot, _)) if slot.inventory.owner eq brainInventory =>
      sync(slot.index, SyncDirection.BrainToDesktop)
  }

  override def onItemAdded(slot: Slot): Unit = {
    if (!isLoading) {
      sync(slot.index, SyncDirection.DesktopToBrain)
    }
  }

  override def onItemRemoved(slot: Slot, removedItem: I, replacedBy: Option[I]): Unit = {
    if (!isLoading) {
      sync(slot.index, SyncDirection.DesktopToBrain)
    }
  }

  def syncSlots(direction: SyncDirection = SyncDirection.Reconcile): Unit = {
    val occupiedSlots = inventoryIterator
      .map(_.index)
      .toSet
      .union(brainInventory.inventory.iterator.map(_.index).toSet)

    for (slotIndex <- occupiedSlots)
      sync(slotIndex, direction)
  }

  private def sync(slotIndex: Int, direction: SyncDirection): Unit = {
    val initialSync = syncFuel == InitialSyncFuel

    try {
      doSync(slotIndex, direction)
    } finally {
      if (initialSync)
        refuel()
    }
  }

  private def refuel(): Unit = {
    syncFuel = InitialSyncFuel
  }

  @tailrec
  private def doSync(slotIndex: Int, direction: SyncDirection): Unit = {
    syncFuel -= 1

    if (syncFuel < 0) {
      // ignore: the limit has already been reached
    } else if (syncFuel == 0) {
      logger.error(
        s"Got trapped in an infinite loop while trying to synchronize the slot $slotIndex " +
          s"in $this (class ${this.getClass.getName})!"
      )
      logger.error(
        "The item in the slot: " +
          Slot(slotIndex).get.map(item => s"$item (class ${item.getClass.getName})").getOrElse("<empty>")
      )
      logger.error(
        "The entity if the slot: " +
          brainInventory
            .inventory(slotIndex)
            .get
            .map(entity => s"$entity (class ${entity.getClass.getName})")
            .getOrElse("<empty>")
      )

      logger.error("Breaking the loop forcefully by removing the items.")
      Slot(slotIndex).removeAndDispose()
      brainInventory.inventory(slotIndex).remove()
    } else {
      direction match {
        case _ if checkSlotStatus(slotIndex) == SlotStatus.Synchronized =>

        case SyncDirection.DesktopToBrain =>
          // the `asInstanceOf` is indeed entirely superfluous, but IntelliJ IDEA complains otherwise...
          OcelotDesktop.withTickLockAcquired {
            brainInventory.inventory(slotIndex).set(Slot(slotIndex).get.map(_.asInstanceOf[EntityItem].entity))
          }

        case SyncDirection.BrainToDesktop =>
          val item = brainInventory.inventory(slotIndex).get match {
            case Some(entity) =>
              Items.recover(entity) match {
                case Some(item) => Some(item.asInstanceOf[I])

                case None =>
                  logger.error(
                    s"An entity ($entity class ${entity.getClass.getName}) was inserted into a slot " +
                      s"(index: $slotIndex) of a brain inventory $brainInventory, " +
                      s"but we were unable to recover an Item from it."
                  )
                  logger.error(
                    s"A Desktop inventory $this (class ${getClass.getName}) could not recover the item. Removing."
                  )
                  logEntityLoss(slotIndex, entity)

                  None
              }

            case None => None
          }

          Slot(slotIndex).set(item)

        case SyncDirection.Reconcile =>
          checkSlotStatus(slotIndex) match {
            case SlotStatus.Synchronized => // no-op
            // let's just grab whatever we have
            case SlotStatus.DesktopNonEmpty => doSync(slotIndex, SyncDirection.DesktopToBrain)
            case SlotStatus.BrainNonEmpty => doSync(slotIndex, SyncDirection.BrainToDesktop)

            case SlotStatus.Conflict =>
              // so, the brain inventory and the Desktop inventory have conflicting views on what the slot contains
              // we'll let Desktop win because it has more info

              (brainInventory.inventory(slotIndex).get, Slot(slotIndex).get) match {
                case (Some(entity), Some(item)) =>
                  logger.error(
                    s"Encountered an inventory conflict for slot $slotIndex! " +
                      s"The Desktop inventory believes the slot contains $item (class ${item.getClass.getName}), " +
                      s"but the brain inventory believes the slot contains $entity (class ${entity.getClass.getName})."
                  )
                  logger.error("Resolving the conflict in favor of Ocelot Desktop.")

                case _ =>
                  // this really should not happen... but alright, let's throw an exception at least
                  throw new IllegalStateException(
                    "an inventory conflict was detected even though one of the slots is empty"
                  )
              }

              doSync(slotIndex, SyncDirection.DesktopToBrain)
          }
      }
    }
  }

  private def logEntityLoss(slotIndex: Int, entity: Entity): Unit = {
    logger.error(
      s"Encountered a data loss! " +
        s"In the brain inventory $brainInventory (class ${brainInventory.getClass.getName}), " +
        s"the entity $entity (class ${entity.getClass.getName}) is deleted from the slot $slotIndex."
    )
  }

  private def checkSlotStatus(slotIndex: Int): SlotStatus = {
    (Slot(slotIndex).get, brainInventory.inventory(slotIndex).get) match {
      case (Some(item), Some(entity)) if item.entity eq entity => SlotStatus.Synchronized
      case (Some(_), Some(_)) => SlotStatus.Conflict
      case (Some(_), None) => SlotStatus.DesktopNonEmpty
      case (None, Some(_)) => SlotStatus.BrainNonEmpty
      case (None, None) => SlotStatus.Synchronized
    }
  }
}

object SyncedInventory {
  private val SlotEntityAddressTag = "entity"
  private val SlotEntityIdTag = "entity_id"

  private val InitialSyncFuel = 5

  object SyncDirection extends Enumeration {
    type SyncDirection = Value

    /** Apply a change from the [[SyncedInventory]] to [[SyncedInventory.brainInventory]]. */
    val DesktopToBrain: SyncDirection = Value

    /** Apply a change from the [[SyncedInventory.brainInventory]] to [[SyncedInventory]]. */
    val BrainToDesktop: SyncDirection = Value

    /** Try to reconcile conflicts between [[SyncedInventory]] and its [[SyncedInventory.brainInventory]]. */
    val Reconcile: SyncDirection = Value
  }

  object SlotStatus extends Enumeration {
    type SlotStatus = Value

    /** The slots are in sync (both are empty or they contain the same entity). */
    val Synchronized: SlotStatus = Value

    /** [[SyncedInventory]]'s slot is non-empty; [[SyncedInventory.brainInventory]]'s is empty. */
    val DesktopNonEmpty: SlotStatus = Value

    /** [[SyncedInventory]]'s slot is empty; [[SyncedInventory.brainInventory]]'s isn't. */
    val BrainNonEmpty: SlotStatus = Value

    /** The [[SyncedInventory]] and the [[SyncedInventory.brainInventory]] are both non-empty
      * and contain different items.
      */
    val Conflict: SlotStatus = Value
  }
}
