package ocelot.desktop.util

import ocelot.desktop.entity.traits.OcelotInterface
import ocelot.desktop.ui.event.{BrainEvent, EventAware}
import ocelot.desktop.ui.widget.LogWidget.{LogEntry, TextLogEntry}
import ocelot.desktop.ui.widget.window.Windowed
import ocelot.desktop.util.OcelotInterfaceLogStorage._
import ocelot.desktop.windows.OcelotInterfaceWindow
import totoro.ocelot.brain.nbt.ExtendedNBT.extendNBTTagList
import totoro.ocelot.brain.nbt.{NBT, NBTBase, NBTTagCompound, NBTTagString}

import scala.collection.mutable
import scala.jdk.CollectionConverters.BufferHasAsJava

trait OcelotInterfaceLogStorage extends EventAware with Persistable with Windowed[OcelotInterfaceWindow] with Logging {

  def ocelotInterface: OcelotInterface

  def name: String

  override def createWindow(): OcelotInterfaceWindow = new OcelotInterfaceWindow(this)

  private var _messageLimit: Int = 1000

  private val _entries = mutable.ArrayDeque.empty[LogEntry]

  eventHandlers += {
    case BrainEvent(OcelotInterface.LogEvent.Clear(_)) =>
      clear()

    case BrainEvent(OcelotInterface.LogEvent.CardToUser(_, message)) =>
      addEntry(LogEntry.Rx(message))

    case BrainEvent(OcelotInterface.LogEvent.UserToCard(_, message)) =>
      addEntry(LogEntry.Tx(message))
  }

  private def loadEntry(nbt: NBTTagCompound): Either[String, LogEntry] = {
    nbt.getString(EntryKindTag) match {
      case EntryKindRx => Right(LogEntry.Rx(nbt.getString(EntryMessageTag)))
      case EntryKindTx => Right(LogEntry.Tx(nbt.getString(EntryMessageTag)))

      case "" => Left("entry kind not set")
      case k => Left(s"unknown entry kind: $k")
    }
  }

  private def saveEntry(entry: LogEntry): NBTTagCompound = {
    val result = new NBTTagCompound()

    entry match {
      case LogEntry.Rx(message) =>
        result.setString(EntryKindTag, EntryKindRx)
        result.setString(EntryMessageTag, message)

      case LogEntry.Tx(message) =>
        result.setString(EntryKindTag, EntryKindTx)
        result.setString(EntryMessageTag, message)
    }

    result
  }

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

    clear()

    if (nbt.hasKey(MessageLimitTag)) {
      messageLimit = nbt.getInteger(MessageLimitTag)
    }

    val entries = if (nbt.hasKey(EntriesTag)) {
      nbt.getTagList(EntriesTag, NBT.TAG_COMPOUND).iterator[NBTTagCompound].zipWithIndex.flatMap {
        case (entryNbt, idx) =>
          loadEntry(entryNbt) match {
            case Left(err) =>
              logger.warn(
                s"Could not restore log entry (idx $idx) of ocelot interface ${ocelotInterface.node.address}:" +
                  s" $err"
              )

              None

            case Right(entry) => Some(entry)
          }
      }
    } else {
      // old save format: plain messages
      nbt
        .getTagList(MessagesTag, NBT.TAG_STRING)
        .iterator[NBTTagString]
        .map(entryNbt => LogEntry.Rx(entryNbt.getString))
    }

    addEntries(entries.toSeq)
  }

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

    nbt.setTagList(EntriesTag, _entries.map(saveEntry(_).asInstanceOf[NBTBase]).asJava)
    nbt.setInteger(MessageLimitTag, _messageLimit)
  }

  def messageLimit: Int = _messageLimit

  def messageLimit_=(limit: Int): Unit = {
    require(limit > 0)
    ensureFreeSpace(_entries.length - limit)
    _messageLimit = limit
  }

  def entryCount: Int = {
    _entries.length
  }

  def clear(): Unit = {
    val count = _entries.length
    _entries.clear()
    window.onMessagesRemoved(count)
  }

  def getEntry(index: Int): LogEntry = _entries(index)

  def findFirstTextEntry(exclude: String, start: Int): Int = {
    _entries.indexWhere(it => it.isInstanceOf[TextLogEntry] && it.asInstanceOf[TextLogEntry].message != exclude, start)
  }

  def findLastTextEntry(exclude: String, end: Int): Int = {
    _entries.lastIndexWhere(it => it.isInstanceOf[TextLogEntry] && it.asInstanceOf[TextLogEntry].message != exclude, end)
  }

  private def addEntry(entry: LogEntry): Unit = {
    ensureFreeSpace(1)
    _entries += entry
    onMessagesAdded(Some(entry))
  }

  private def addEntries(entries: Seq[LogEntry]): Unit = {
    ensureFreeSpace(entries.length)
    val prevCount = _entries.length
    _entries ++= entries.view.takeRight(messageLimit)
    onMessagesAdded(_entries.view.takeRight(_entries.length - prevCount))
  }

  private def ensureFreeSpace(n: Int): Unit = {
    val prevCount = _entries.length
    _entries.takeRightInPlace(messageLimit - n)
    val removedCount = prevCount - _entries.length

    if (removedCount > 0) {
      window.onMessagesRemoved(removedCount)
    }
  }

  protected def onMessagesAdded(entries: => Iterable[LogEntry]): Unit = {
    window.onMessagesAdded(entries)
  }
}

object OcelotInterfaceLogStorage {
  private val MessagesTag = "messages"
  private val EntriesTag = "entries"
  private val MessageLimitTag = "limit"

  private val EntryKindTag = "kind"
  private val EntryKindRx = "rx"
  private val EntryKindTx = "tx"
  private val EntryMessageTag = "msg"
}
