package ocelot.desktop.inventory

import ocelot.desktop.inventory.Inventory.SlotObserver
import ocelot.desktop.ui.event.{BrainEvent, Dispatchable, EventAware}
import ocelot.desktop.util.Disposable
import totoro.ocelot.brain.event.NodeEvent

import scala.collection.mutable

/**
  * Provides an inventory — a collection of [[Item]]s indexed by slots.
  */
trait Inventory extends EventAware with Disposable {
  // parallels totoro.ocelot.brain.entity.traits.Inventory
  // this is intentional

  /**
    * The type of items stored in this inventory.
    */
  type I <: Item

  private type WeakHashSet[A] = mutable.WeakHashMap[A, Unit]

  private val slotItems = mutable.HashMap.empty[Int, I]
  private val itemSlots = mutable.HashMap.empty[I, Int]
  private val observers = mutable.HashMap.empty[Int, WeakHashSet[SlotObserver]]

  override def dispose(): Unit = {
    super.dispose()

    // NOTE: we dispose of, but do not remove items here, as it would produce unwanted side effects (like wiping RAIDs).
    for (slot <- inventoryIterator; item <- slot.get) {
      item.dispose()
    }
  }

  /**
    * Called after a new item is added to the inventory.
    *
    * @param slot the slot the item was added to
    */
  def onItemAdded(slot: Slot): Unit

  /**
    * Called after an item is removed from the inventory.
    *
    * When the item is replaced by another one, the event are sequenced in the following order:
    *
    *   1. the old item is removed
    *   1. [[onItemRemoved]] is called, with `replacedBy` containing the new item
    *   1. the new item is added
    *   1. [[onItemAdded]] is called
    *
    * @param slot        the slot the item was removed from
    * @param removedItem the previously present item
    * @param replacedBy  if known, the item it was replaced by ([[onItemAdded]] is still called)
    */
  def onItemRemoved(slot: Slot, removedItem: I, replacedBy: Option[I]): Unit

  /**
    * An iterator over all slots occupied in this inventory.
    */
  def inventoryIterator: Iterator[Slot] = slotItems.keysIterator.map(Slot(_))

  def clearInventory(): Unit = {
    for (slot <- slotItems.keys.toArray) {
      Slot(slot).remove()
    }
  }

  private def slotObservers(slotIndex: Int): Iterator[SlotObserver] =
    observers.get(slotIndex).iterator.flatMap(_.view.keys)

  private def onItemAddedImpl(slot: Slot): Unit = {
    onItemAdded(slot)
    slotObservers(slot.index).foreach(_.onItemAdded())
  }

  private def onItemRemovedImpl(slot: Slot, removedItem: I, replacedBy: Option[I]): Unit = {
    onItemRemoved(slot, removedItem, replacedBy)
    slotObservers(slot.index).foreach(_.onItemRemoved(removedItem, replacedBy))
  }

  override def shouldReceiveEventsFor(address: String): Boolean =
    super.shouldReceiveEventsFor(address) ||
      inventoryIterator.flatMap(_.get).exists(_.shouldReceiveEventsFor(address))

  override def handleEvent(event: Dispatchable): Unit = {
    super.handleEvent(event)

    for (slot <- inventoryIterator; item <- slot.get) {
      event match {
        case BrainEvent(e: NodeEvent) if !item.shouldReceiveEventsFor(e.address) => // ignore
        case _ => item.handleEvent(event)
      }
    }
  }

  /**
    * A proxy to access a slot of the inventory.
    */
  final class Slot private[Inventory] (val index: Int) {
    require(index >= 0)

    def isEmpty: Boolean = get.isEmpty

    def nonEmpty: Boolean = !isEmpty

    /**
      * Inserts the `item` into this slot (replacing the previous item if any).
      */
    def put(item: inventory.I): Unit = {
      setSlot(index, Some(item))
    }

    /**
      * Allows inserting/removing the item in this slot.
      */
    def set(item: Option[inventory.I]): Unit = {
      setSlot(index, item)
    }

    /**
      * Removes the item contained in this slot if there is one.
      */
    def remove(): Unit = {
      setSlot(index, None)
    }

    /**
      * Removes the item contained in this slot if there is one, calling the item's [[dispose]] method.
      */
    def removeAndDispose(): Unit = {
      for (item <- get) {
        remove()
        item.dispose()
      }
    }

    /**
      * The [[Item]] contained in this slot.
      */
    def get: Option[inventory.I] = slotItems.get(index)

    val inventory: Inventory.this.type = Inventory.this

    /**
      * Registers an observer to receive item added/removed events.
      *
      * @note The inventory keeps a '''weak''' reference to the `observer`.
      */
    def addObserver(observer: SlotObserver): Unit = {
      observers.getOrElseUpdate(index, new WeakHashSet) += observer -> ()
    }

    def removeObserver(observer: SlotObserver): Unit = {
      for (slotObservers <- observers.get(index)) {
        slotObservers.remove(observer)

        if (slotObservers.isEmpty) {
          observers.remove(index)
        }
      }
    }

    private[inventory] def notifySlot(notification: Item.Notification): Unit = {
      slotObservers(index).foreach(_.onItemNotification(notification))
    }

    override def equals(other: Any): Boolean = other match {
      case that: Inventory#Slot =>
        // in case you're wondering wtf this is:
        // IntelliJ IDEA has a bug that wrongly rejects `inventory == that.inventory` due to a typing error
        // (the code is accepted by the compiler though)
        // stripping this.type seems to help
        val thisInv = classOf[Inventory].cast(inventory)
        val thatInv = classOf[Inventory].cast(that.inventory)
        thisInv == thatInv && index == that.index

      case _ => false
    }

    override def hashCode(): Int = {
      val state = Seq(inventory, index)
      state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)
    }
  }

  object Slot {
    /**
      * Creates a proxy to an inventory slot.
      */
    def apply(index: Int) = new Slot(index)
  }

  private def setSlot(index: Int, item: Option[I]): Unit = {
    val slot = Slot(index)

    (slotItems.get(index), item) match {
      case (Some(oldItem), Some(newItem)) if oldItem == newItem =>
      // no-op

      case (Some(oldItem), Some(newItem)) =>
        // replace old with new
        doRemove(index)
        onItemRemovedImpl(slot, oldItem, Some(newItem))
        doInsert(index, newItem)
        onItemAddedImpl(slot)

      case (Some(oldItem), None) =>
        // remove old
        doRemove(index)
        onItemRemovedImpl(slot, oldItem, None)

      case (None, Some(newItem)) =>
        // add new
        doInsert(index, newItem)
        onItemAddedImpl(slot)

      case (None, None) =>
      // no-op
    }
  }

  private def doRemove(index: Int): Unit = {
    for (oldItem <- slotItems.remove(index)) {
      assert(oldItem.slot.exists(_.index == index))
      oldItem.slot = None

      val oldIndex = itemSlots.remove(oldItem)
      assert(oldIndex.contains(index))
    }
  }

  private def doInsert(index: Int, item: I): Unit = {
    assert(!itemSlots.contains(item))
    assert(!slotItems.contains(index))
    assert(item.slot.isEmpty)

    slotItems(index) = item
    itemSlots(item) = index
    item.slot = Some(Slot(index))
  }
}

object Inventory {
  trait SlotObserver {

    /**
      * Called after an item was inserted into this slot.
      *
      * @note [[Inventory.onItemAdded]] is called before this method.
      */
    def onItemAdded(): Unit

    /**
      * Called after an item was removed from this slot.
      *
      * In particular, the slot no longer contains the removed item.
      *
      * @note [[Inventory.onItemRemoved]] is called before this method.
      */
    def onItemRemoved(removedItem: Item, replacedBy: Option[Item]): Unit

    /**
      * Called when an item contained in this slot sends a notification via [[Item.notifySlot]].
      */
    def onItemNotification(notification: Item.Notification): Unit
  }
}
