package li.cil.oc.integration.tis3d

import li.cil.oc.Settings
import li.cil.oc.api
import li.cil.oc.api.internal.Adapter
import li.cil.oc.api.machine.Arguments
import li.cil.oc.api.machine.Callback
import li.cil.oc.api.machine.Context
import li.cil.oc.api.network.Environment
import li.cil.oc.api.network.Message
import li.cil.oc.api.network.Node
import li.cil.oc.api.network.Visibility
import li.cil.oc.util.ResultWrapper.result
import li.cil.tis3d.api.ManualAPI
import li.cil.tis3d.api.SerialAPI
import li.cil.tis3d.api.prefab.manual.ResourceContentProvider
import li.cil.tis3d.api.serial.SerialInterface
import li.cil.tis3d.api.serial.SerialInterfaceProvider
import li.cil.tis3d.api.serial.SerialProtocolDocumentationReference
import net.minecraft.nbt.NBTTagCompound
import net.minecraft.util.EnumFacing
import net.minecraft.world.World

import scala.collection.mutable

object SerialInterfaceProviderAdapter extends SerialInterfaceProvider {
  def init(): Unit = {
    ManualAPI.addProvider(new ResourceContentProvider(Settings.resourceDomain, "doc/tis3d/"))
    SerialAPI.addProvider(this)
  }

  override def getDocumentationReference = new SerialProtocolDocumentationReference("OpenComputers Adapter", "protocols/opencomputersAdapter.md")

  override def worksWith(world: World, x: Int, y: Int, z: Int, side: EnumFacing): Boolean = world.getTileEntity(x, y, z).isInstanceOf[Adapter]

  override def interfaceFor(world: World, x: Int, y: Int, z: Int, side: EnumFacing): SerialInterface = new SerialInterfaceAdapter(world.getTileEntity(x, y, z).asInstanceOf[Adapter])

  override def isValid(world: World, x: Int, y: Int, z: Int, side: EnumFacing, serialInterface: SerialInterface): Boolean = serialInterface match {
    case adapter: SerialInterfaceAdapter => adapter.tileEntity == world.getTileEntity(x, y, z)
    case _ => false
  }

  class SerialInterfaceAdapter(val tileEntity: Adapter) extends Environment with SerialInterface {
    final val BufferCapacity = 128
    final val readBuffer = mutable.Queue.empty[Short]
    final val writeBuffer = mutable.Queue.empty[Short]
    var isReading = false

    // ----------------------------------------------------------------------- //

    val node = api.Network.newNode(this, Visibility.Network).withComponent("serial_port").create()

    override def onMessage(message: Message): Unit = {}

    override def onConnect(node: Node): Unit = {}

    override def onDisconnect(node: Node): Unit = {}

    // ----------------------------------------------------------------------- //

    @Callback
    def setReading(context: Context, args: Arguments): Array[AnyRef] = {
      isReading = args.checkBoolean(0)
      null
    }

    @Callback
    def read(context: Context, args: Arguments): Array[AnyRef] = {
      readBuffer.synchronized(if (readBuffer.nonEmpty) {
        result(readBuffer.dequeue())
      } else {
        null
      })
    }

    @Callback
    def write(context: Context, args: Arguments): Array[AnyRef] = {
      writeBuffer.synchronized(if (writeBuffer.length < BufferCapacity) {
        writeBuffer += args.checkInteger(0).toShort
        result(true)
      } else {
        result(false, "buffer full")
      })
    }

    // ----------------------------------------------------------------------- //

    override def canWrite: Boolean = readBuffer.synchronized(isReading && readBuffer.length < BufferCapacity)

    override def write(value: Short): Unit = readBuffer.synchronized(readBuffer += value)

    override def canRead: Boolean = {
      ensureConnected()
      writeBuffer.synchronized(writeBuffer.nonEmpty)
    }

    override def peek(): Short = writeBuffer.synchronized(writeBuffer.front)

    override def skip(): Unit = writeBuffer.synchronized(writeBuffer.dequeue())

    override def reset(): Unit = {
      readBuffer.synchronized(writeBuffer.synchronized {
        readBuffer.clear()
        writeBuffer.clear()
        node.remove()
      })
    }

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

      writeBuffer.clear()
      writeBuffer ++= nbt.getIntArray("writeBuffer").map(_.toShort)
      readBuffer.clear()
      readBuffer ++= nbt.getIntArray("readBuffer").map(_.toShort)
      isReading = nbt.getBoolean("isReading")
    }

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

      nbt.setIntArray("writeBuffer", writeBuffer.toArray.map(_.toInt))
      nbt.setIntArray("readBuffer", readBuffer.toArray.map(_.toInt))
      nbt.setBoolean("isReading", isReading)
    }

    private def ensureConnected(): Unit = {
      if (tileEntity.node.network != node.network) {
        tileEntity.node.connect(node)
      }
    }
  }

}
