package li.cil.oc.common.tileentity

import java.util

import cpw.mods.fml.relauncher.Side
import cpw.mods.fml.relauncher.SideOnly
import li.cil.oc.Constants
import li.cil.oc.api.driver.DeviceInfo.DeviceAttribute
import li.cil.oc.api.driver.DeviceInfo.DeviceClass
import li.cil.oc.Settings
import li.cil.oc.api
import li.cil.oc.api.driver.DeviceInfo
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._
import li.cil.oc.common.item.data.PrintData
import li.cil.oc.server.{PacketSender => ServerPacketSender}
import li.cil.oc.util.ExtendedNBT._
import net.minecraft.inventory.ISidedInventory
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NBTTagCompound
import net.minecraft.util.AxisAlignedBB
import net.minecraftforge.common.util.ForgeDirection

import scala.collection.convert.WrapAsJava._

class Printer extends traits.Environment with traits.Inventory with traits.Rotatable with SidedEnvironment with traits.StateAware with ISidedInventory with DeviceInfo {
  val node = api.Network.newNode(this, Visibility.Network).
    withComponent("printer3d").
    withConnector(Settings.get.bufferConverter).
    create()

  val maxAmountMaterial = 256000
  var amountMaterial = 0
  val maxAmountInk = 100000
  var amountInk = 0

  var data = new PrintData()
  var isActive = false
  var limit = 0
  var output: Option[ItemStack] = None
  var totalRequiredEnergy = 0.0
  var requiredEnergy = 0.0

  val slotMaterial = 0
  val slotInk = 1
  val slotOutput = 2

  private final lazy val deviceInfo = Map(
    DeviceAttribute.Class -> DeviceClass.Printer,
    DeviceAttribute.Description -> "3D Printer",
    DeviceAttribute.Vendor -> Constants.DeviceInfo.DefaultVendor,
    DeviceAttribute.Product -> "Omni-Materializer T6.1"
  )

  override def getDeviceInfo: util.Map[String, String] = deviceInfo

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

  @SideOnly(Side.CLIENT)
  override def canConnect(side: ForgeDirection) = side != ForgeDirection.UP

  override def sidedNode(side: ForgeDirection) = if (side != ForgeDirection.UP) node else null

  override def getCurrentState = {
    if (isPrinting) util.EnumSet.of(api.util.StateAware.State.IsWorking)
    else if (canPrint) util.EnumSet.of(api.util.StateAware.State.CanWork)
    else util.EnumSet.noneOf(classOf[api.util.StateAware.State])
  }

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

  def canPrint = data.stateOff.nonEmpty && data.stateOff.size <= Settings.get.maxPrintComplexity && data.stateOn.size <= Settings.get.maxPrintComplexity

  def isPrinting = output.isDefined

  def progress = (1 - requiredEnergy / totalRequiredEnergy) * 100

  def timeRemaining = (requiredEnergy / Settings.get.assemblerTickAmount / 20).toInt

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

  @Callback(doc = """function() -- Resets the configuration of the printer and stop printing (current job will finish).""")
  def reset(context: Context, args: Arguments): Array[Object] = {
    data = new PrintData()
    isActive = false // Needs committing.
    null
  }

  @Callback(doc = """function(value:string) -- Set a label for the block being printed.""")
  def setLabel(context: Context, args: Arguments): Array[Object] = {
    data.label = Option(args.optString(0, null)).map(_.take(24))
    if (data.label.fold(false)(_.isEmpty)) data.label = None
    isActive = false // Needs committing.
    null
  }

  @Callback(doc = """function():string -- Get the current label for the block being printed.""")
  def getLabel(context: Context, args: Arguments): Array[Object] = {
    result(data.label.orNull)
  }

  @Callback(doc = """function(value:string) -- Set a tooltip for the block being printed.""")
  def setTooltip(context: Context, args: Arguments): Array[Object] = {
    data.tooltip = Option(args.optString(0, null)).map(_.take(128))
    if (data.tooltip.fold(false)(_.isEmpty)) data.tooltip = None
    isActive = false // Needs committing.
    null
  }

  @Callback(doc = """function():string -- Get the current tooltip for the block being printed.""")
  def getTooltip(context: Context, args: Arguments): Array[Object] = {
    result(data.tooltip.orNull)
  }

  @Callback(doc = """function(value:number) -- Set what light level the printed block should have.""")
  def setLightLevel(context: Context, args: Arguments): Array[Object] = {
    data.lightLevel = args.checkInteger(0) max 0 min Settings.get.maxPrintLightLevel
    isActive = false // Needs committing.
    null
  }

  @Callback(doc = """function():number -- Get which light level the printed block should have.""")
  def getLightLevel(context: Context, args: Arguments): Array[Object] = {
    result(data.lightLevel)
  }

  @Callback(doc = """function(value:boolean or number) -- Set whether the printed block should emit redstone when in its active state.""")
  def setRedstoneEmitter(context: Context, args: Arguments): Array[Object] = {
    if (args.isBoolean(0)) data.redstoneLevel = if (args.checkBoolean(0)) 15 else 0
    else data.redstoneLevel = args.checkInteger(0) max 0 min 15
    isActive = false // Needs committing.
    null
  }

  @Callback(doc = """function():boolean, number -- Get whether the printed block should emit redstone when in its active state.""")
  def isRedstoneEmitter(context: Context, args: Arguments): Array[Object] = {
    result(data.emitRedstone, data.redstoneLevel)
  }

  @Callback(doc = """function(value:boolean) -- Set whether the printed block should automatically return to its off state.""")
  def setButtonMode(context: Context, args: Arguments): Array[Object] = {
    data.isButtonMode = args.checkBoolean(0)
    isActive = false // Needs committing.
    null
  }

  @Callback(doc = """function():boolean -- Get whether the printed block should automatically return to its off state.""")
  def isButtonMode(context: Context, args: Arguments): Array[Object] = {
    result(data.isButtonMode)
  }

  @Callback(doc = """function(collideOff:boolean, collideOn:boolean) -- Set whether the printed block should be collidable or not.""")
  def setCollidable(context: Context, args: Arguments): Array[Object] = {
    val (collideOff, collideOn) = (args.checkBoolean(0), args.checkBoolean(1))
    data.noclipOff = !collideOff
    data.noclipOn = !collideOn
    null
  }

  @Callback(doc = """function():boolean, boolean -- Get whether the printed block should be collidable or not.""")
  def isCollidable(context: Context, args: Arguments): Array[Object] = {
    result(!data.noclipOff, !data.noclipOn)
  }

  @Callback(doc = """function(minX:number, minY:number, minZ:number, maxX:number, maxY:number, maxZ:number, texture:string[, state:boolean=false][,tint:number]) -- Adds a shape to the printers configuration, optionally specifying whether it is for the off or on state.""")
  def addShape(context: Context, args: Arguments): Array[Object] = {
    if (data.stateOff.size > Settings.get.maxPrintComplexity || data.stateOn.size > Settings.get.maxPrintComplexity) {
      return result(Unit, "model too complex")
    }
    val minX = (args.checkInteger(0) max 0 min 16) / 16f
    val minY = (args.checkInteger(1) max 0 min 16) / 16f
    val minZ = (16 - (args.checkInteger(2) max 0 min 16)) / 16f
    val maxX = (args.checkInteger(3) max 0 min 16) / 16f
    val maxY = (args.checkInteger(4) max 0 min 16) / 16f
    val maxZ = (16 - (args.checkInteger(5) max 0 min 16)) / 16f
    val texture = args.checkString(6).take(64)
    val state = if (args.isBoolean(7)) args.checkBoolean(7) else false
    val tint = if (args.isInteger(7)) Option(args.checkInteger(7)) else if (args.isInteger(8)) Option(args.checkInteger(8)) else None

    if (minX == maxX) throw new IllegalArgumentException("empty block")
    if (minY == maxY) throw new IllegalArgumentException("empty block")
    if (minZ == maxZ) throw new IllegalArgumentException("empty block")

    val list = if (state) data.stateOn else data.stateOff
    list += new PrintData.Shape(AxisAlignedBB.getBoundingBox(
      math.min(minX, maxX),
      math.min(minY, maxY),
      math.min(minZ, maxZ),
      math.max(maxX, minX),
      math.max(maxY, minY),
      math.max(maxZ, minZ)),
      texture, tint)
    isActive = false // Needs committing.

    world.markBlockForUpdate(x, y, z)

    result(true)
  }

  @Callback(doc = """function():number -- Get the number of shapes in the current configuration.""")
  def getShapeCount(context: Context, args: Arguments): Array[Object] = result(data.stateOff.size, data.stateOn.size)

  @Callback(doc = """function():number -- Get the maximum allowed number of shapes.""")
  def getMaxShapeCount(context: Context, args: Arguments): Array[Object] = result(Settings.get.maxPrintComplexity)

  @Callback(doc = """function([count:number]):boolean -- Commit and begin printing the current configuration.""")
  def commit(context: Context, args: Arguments): Array[Object] = {
    if (!canPrint) {
      return result(Unit, "model invalid")
    }
    limit = (args.optDouble(0, 1) max 0 min Integer.MAX_VALUE).toInt
    isActive = limit > 0
    result(true)
  }

  @Callback(doc = """function(): string, number or boolean -- The current state of the printer, `busy' or `idle', followed by the progress or model validity, respectively.""")
  def status(context: Context, args: Arguments): Array[Object] = {
    if (isPrinting) result("busy", progress)
    else if (canPrint) result("idle", true)
    else result("idle", false)
  }

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

  override def canUpdate = isServer

  override def updateEntity() {
    super.updateEntity()

    def canMergeOutput = {
      val presentStack = getStackInSlot(slotOutput)
      val outputStack = data.createItemStack()
      presentStack == null || (presentStack.isItemEqual(outputStack) && ItemStack.areItemStackTagsEqual(presentStack, outputStack))
    }

    if (isActive && output.isEmpty && canMergeOutput) {
      PrintData.computeCosts(data) match {
        case Some((materialRequired, inkRequired)) =>
          totalRequiredEnergy = Settings.get.printCost
          requiredEnergy = totalRequiredEnergy

          if (amountMaterial >= materialRequired && amountInk >= inkRequired) {
            amountMaterial -= materialRequired
            amountInk -= inkRequired
            limit -= 1
            output = Option(data.createItemStack())
            if (limit < 1) isActive = false
            ServerPacketSender.sendPrinting(this, printing = true)
          }
        case _ =>
          isActive = false
          data = new PrintData()
      }
    }

    if (output.isDefined) {
      val want = math.max(1, math.min(requiredEnergy, Settings.get.printerTickAmount))
      val have = want + (if (Settings.get.ignorePower) 0 else node.changeBuffer(-want))
      requiredEnergy -= have
      if (requiredEnergy <= 0) {
        val result = getStackInSlot(slotOutput)
        if (result == null) {
          setInventorySlotContents(slotOutput, output.get)
        }
        else if (result.stackSize < result.getMaxStackSize && canMergeOutput /* Should never fail, but just in case... */ ) {
          result.stackSize += 1
          markDirty()
        }
        else {
          return
        }
        requiredEnergy = 0
        output = None
      }
      ServerPacketSender.sendPrinting(this, have > 0.5 && output.isDefined)
    }

    val inputValue = PrintData.materialValue(getStackInSlot(slotMaterial))
    if (inputValue > 0 && maxAmountMaterial - amountMaterial >= inputValue) {
      val material = decrStackSize(slotMaterial, 1)
      if (material != null) {
        amountMaterial += inputValue
      }
    }

    val inkValue = PrintData.inkValue(getStackInSlot(slotInk))
    if (inkValue > 0 && maxAmountInk - amountInk >= inkValue) {
      val material = decrStackSize(slotInk, 1)
      if (material != null) {
        amountInk += inkValue
        if (material.getItem.hasContainerItem(material)) {
          setInventorySlotContents(slotInk, material.getItem.getContainerItem(material))
        }
      }
    }
  }

  override def readFromNBTForServer(nbt: NBTTagCompound) {
    super.readFromNBTForServer(nbt)
    amountMaterial = nbt.getInteger(Settings.namespace + "amountMaterial")
    amountInk = nbt.getInteger(Settings.namespace + "amountInk")
    data.load(nbt.getCompoundTag(Settings.namespace + "data"))
    isActive = nbt.getBoolean(Settings.namespace + "active")
    limit = nbt.getInteger(Settings.namespace + "limit")
    if (nbt.hasKey(Settings.namespace + "output")) {
      output = Option(ItemStack.loadItemStackFromNBT(nbt.getCompoundTag(Settings.namespace + "output")))
    }
    totalRequiredEnergy = nbt.getDouble(Settings.namespace + "total")
    requiredEnergy = nbt.getDouble(Settings.namespace + "remaining")
  }

  override def writeToNBTForServer(nbt: NBTTagCompound) {
    super.writeToNBTForServer(nbt)
    nbt.setInteger(Settings.namespace + "amountMaterial", amountMaterial)
    nbt.setInteger(Settings.namespace + "amountInk", amountInk)
    nbt.setNewCompoundTag(Settings.namespace + "data", data.save)
    nbt.setBoolean(Settings.namespace + "active", isActive)
    nbt.setInteger(Settings.namespace + "limit", limit)
    output.foreach(stack => nbt.setNewCompoundTag(Settings.namespace + "output", stack.writeToNBT))
    nbt.setDouble(Settings.namespace + "total", totalRequiredEnergy)
    nbt.setDouble(Settings.namespace + "remaining", requiredEnergy)
  }

  @SideOnly(Side.CLIENT) override
  def readFromNBTForClient(nbt: NBTTagCompound) {
    super.readFromNBTForClient(nbt)
    data.load(nbt.getCompoundTag(Settings.namespace + "data"))
    requiredEnergy = nbt.getDouble("remaining")
  }

  override def writeToNBTForClient(nbt: NBTTagCompound) {
    super.writeToNBTForClient(nbt)
    nbt.setNewCompoundTag(Settings.namespace + "data", data.save)
    nbt.setDouble("remaining", requiredEnergy)
  }

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

  override def getSizeInventory = 3

  override def isItemValidForSlot(slot: Int, stack: ItemStack) =
    if (slot == slotMaterial)
      PrintData.materialValue(stack) > 0
    else if (slot == slotInk)
      PrintData.inkValue(stack) > 0
    else false

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

  override def getAccessibleSlotsFromSide(side: Int): Array[Int] = Array(slotMaterial, slotInk, slotOutput)

  override def canExtractItem(slot: Int, stack: ItemStack, side: Int): Boolean = !isItemValidForSlot(slot, stack)

  override def canInsertItem(slot: Int, stack: ItemStack, side: Int): Boolean = slot != slotOutput
}
