package edx.basic.engineering

import java.util.ArrayList

import codechicken.multipart.ControlKeyModifer
import cpw.mods.fml.common.network.ByteBufUtils
import cpw.mods.fml.relauncher.{Side, SideOnly}
import edx.basic.blocks.ItemImprint
import edx.basic.process.grinding.ItemHammer
import edx.core.{Electrodynamics, Reference}
import io.netty.buffer.ByteBuf
import net.minecraft.block.Block
import net.minecraft.block.material.Material
import net.minecraft.client.renderer.texture.IIconRegister
import net.minecraft.entity.player.{EntityPlayer, EntityPlayerMP, InventoryPlayer}
import net.minecraft.inventory.{IInventory, ISidedInventory, InventoryCrafting}
import net.minecraft.item.crafting.CraftingManager
import net.minecraft.item.{Item, ItemStack}
import net.minecraft.nbt.{NBTTagCompound, NBTTagList}
import net.minecraft.network.Packet
import net.minecraft.tileentity.TileEntity
import net.minecraft.util.{ChatComponentText, IIcon}
import net.minecraftforge.common.util.ForgeDirection
import net.minecraftforge.oredict.OreDictionary
import org.apache.commons.lang3.ArrayUtils
import org.lwjgl.opengl.GL11
import resonant.api.gui.ISlotPickResult
import resonant.api.recipe.{MachineRecipes, RecipeResource, RecipeType}
import resonant.api.tile.IRotatable
import resonant.core.ResonantEngine
import resonant.lib.collection.Pair
import resonant.lib.network.discriminator.{PacketTile, PacketType}
import resonant.lib.network.handle.IPacketReceiver
import resonant.lib.prefab.gui.ContainerDummy
import resonant.lib.prefab.tile.item.ItemBlockSaved
import resonant.lib.prefab.tile.mixed.TileInventory
import resonant.lib.render.RenderItemOverlayUtility
import resonant.lib.transform.region.Cuboid
import resonant.lib.transform.vector.{Vector2, Vector3}
import resonant.lib.utility.LanguageUtility
import resonant.lib.utility.inventory.AutoCraftingManager.IAutoCrafter
import resonant.lib.utility.inventory.{AutoCraftingManager, InventoryUtility}

import scala.collection.JavaConversions._

/**
 * Advanced crafting table that stores its crafting grid, can craft out of the player's inv, and be
 * configed to auto craft.
 *
 * @author DarkGuardsman, Calclavia
 */
object TileEngineeringTable
{
  val CRAFTING_MATRIX_END: Int = 9
  val CRAFTING_OUTPUT_END: Int = CRAFTING_MATRIX_END + 1
  val PLAYER_OUTPUT_END: Int = CRAFTING_OUTPUT_END + 40
  val CENTER_SLOT: Int = 4
  val CRAFTING_OUTPUT_SLOT: Int = 0
  /**
   * 9 slots for crafting, 1 slot for a output.
   */
  val CRAFTING_MATRIX_SIZE: Int = 9
  val craftingSlots: Array[Int] = Array(0, 1, 2, 3, 4, 5, 6, 7, 8)

  @SideOnly(Side.CLIENT) private var iconTop: IIcon = null
  @SideOnly(Side.CLIENT) private var iconFront: IIcon = null
  @SideOnly(Side.CLIENT) private var iconSide: IIcon = null
}

class TileEngineeringTable extends TileInventory(Material.wood) with IPacketReceiver with IRotatable with ISidedInventory with ISlotPickResult with IAutoCrafter
{

  var craftingMatrix: Array[ItemStack] = new Array[ItemStack](TileEngineeringTable.CRAFTING_MATRIX_SIZE)
  /**
   * The output inventory containing slots.
   */
  var outputInventory: Array[ItemStack] = new Array[ItemStack](1)
  /**
   * The ability for the engineering table to search nearby inventories.
   */
  var searchInventories: Boolean = true
  private var craftManager: AutoCraftingManager = null
  /**
   * Temporary player inventory stored to draw the player's items.
   */
  private var invPlayer: InventoryPlayer = null
  private var playerSlots: Array[Int] = null

  //Constructor
  bounds = new Cuboid(0, 0, 0, 1, 0.9f, 1)
  isOpaqueCube = false
  itemBlock(classOf[ItemBlockSaved])

  @SideOnly(Side.CLIENT)
  override def getIcon(side: Int, meta: Int): IIcon =
  {
    return if (side == 1) TileEngineeringTable.iconTop else (if (side == meta) TileEngineeringTable.iconFront else TileEngineeringTable.iconSide)
  }

  @SideOnly(Side.CLIENT)
  override def registerIcons(iconRegister: IIconRegister)
  {
    TileEngineeringTable.iconTop = iconRegister.registerIcon(getTextureName + "_top")
    TileEngineeringTable.iconFront = iconRegister.registerIcon(getTextureName + "_front")
    TileEngineeringTable.iconSide = iconRegister.registerIcon(getTextureName + "_side")
  }

  override def click(player: EntityPlayer)
  {
    if (!world.isRemote && ControlKeyModifer.isControlDown(player))
    {
      {
        var i: Int = 0
        while (i < getSizeInventory - 1)
        {
          {
            if (getStackInSlot(i) != null)
            {
              InventoryUtility.dropItemStack(world, new Vector3(player), getStackInSlot(i))
              setInventorySlotContents(i, null)
            }
          }
          ({
            i += 1;
            i
          })
        }
      }
    }
  }

  override def getSizeInventory: Int =
  {
    return 10 + (if (this.invPlayer != null) this.invPlayer.getSizeInventory else 0)
  }

  override def setInventorySlotContents(slot: Int, itemStack: ItemStack)
  {
    if (slot < TileEngineeringTable.CRAFTING_MATRIX_END)
    {
      craftingMatrix(slot) = itemStack
    }
    else if (slot < TileEngineeringTable.CRAFTING_OUTPUT_END)
    {
      outputInventory(slot - TileEngineeringTable.CRAFTING_MATRIX_END) = itemStack
    }
    else if (slot < TileEngineeringTable.PLAYER_OUTPUT_END && this.invPlayer != null)
    {
      this.invPlayer.setInventorySlotContents(slot - TileEngineeringTable.CRAFTING_OUTPUT_END, itemStack)
      val player: EntityPlayer = this.invPlayer.player
      if (player.isInstanceOf[EntityPlayerMP])
      {
        (player.asInstanceOf[EntityPlayerMP]).sendContainerToPlayer(player.inventoryContainer)
      }
    }
    else if (searchInventories)
    {
      var idDisplacement: Int = TileEngineeringTable.PLAYER_OUTPUT_END
      for (dir <- ForgeDirection.VALID_DIRECTIONS)
      {
        val tile: TileEntity = toVectorWorld.add(dir).getTileEntity
        if (tile.isInstanceOf[IInventory])
        {
          val inventory: IInventory = tile.asInstanceOf[IInventory]
          val slotID: Int = slot - idDisplacement
          if (slotID >= 0 && slotID < inventory.getSizeInventory)
          {
            inventory.setInventorySlotContents(slotID, itemStack)
          }
          idDisplacement += inventory.getSizeInventory
        }
      }
    }
    onInventoryChanged
  }

  /**
   * Updates all the output slots. Call this to update the Engineering Table.
   */
  override def onInventoryChanged
  {
    if (worldObj != null)
    {
      if (!worldObj.isRemote)
      {
        this.outputInventory(TileEngineeringTable.CRAFTING_OUTPUT_SLOT) = null
        var didCraft: Boolean = false
        val inventoryCrafting: InventoryCrafting = this.getCraftingMatrix
        val matrixOutput: ItemStack = CraftingManager.getInstance.findMatchingRecipe(inventoryCrafting, this.worldObj)
        if (matrixOutput != null && this.getCraftingManager.getIdealRecipe(matrixOutput) != null)
        {
          this.outputInventory(TileEngineeringTable.CRAFTING_OUTPUT_SLOT) = matrixOutput
          didCraft = true
        }
        if (!didCraft)
        {
          val filterStack: ItemStack = craftingMatrix(TileEngineeringTable.CENTER_SLOT)
          if (filterStack != null && filterStack.getItem.isInstanceOf[ItemImprint])
          {
            val filters: java.util.List[ItemStack] = ItemImprint.getFilters(filterStack)
            for (o <- filters)
            {
              val outputStack: ItemStack = o
              if (outputStack != null)
              {
                val idealRecipe: Pair[ItemStack, Array[ItemStack]] = this.getCraftingManager.getIdealRecipe(outputStack)
                if (idealRecipe != null)
                {
                  val recipeOutput: ItemStack = idealRecipe.left
                  if (recipeOutput != null & recipeOutput.stackSize > 0)
                  {
                    this.outputInventory(TileEngineeringTable.CRAFTING_OUTPUT_SLOT) = recipeOutput
                    didCraft = true
                    worldObj.markBlockForUpdate(xCoord, yCoord, zCoord)
                    return
                  }
                }
              }
            }
          }
        }
        worldObj.markBlockForUpdate(xCoord, yCoord, zCoord)
      }
    }
  }

  /**
   * Gets the AutoCraftingManager that does all the crafting results
   */
  def getCraftingManager: AutoCraftingManager =
  {
    if (craftManager == null)
    {
      craftManager = new AutoCraftingManager(this)
    }
    return craftManager
  }

  /**
   * Construct an InventoryCrafting Matrix on the fly.
   *
   * @return
   */
  def getCraftingMatrix: InventoryCrafting =
  {
    val inventoryCrafting: InventoryCrafting = new InventoryCrafting(new ContainerDummy(this), 3, 3)

    for (i <- 0 until this.craftingMatrix.length)
    {
      inventoryCrafting.setInventorySlotContents(i, this.craftingMatrix(i))
    }
    return inventoryCrafting
  }

  /**
   * DO NOT USE THIS INTERNALLY. FOR EXTERNAL USE ONLY!
   */
  override def getStackInSlot(slot: Int): ItemStack =
  {
    if (slot < TileEngineeringTable.CRAFTING_MATRIX_END)
    {
      return this.craftingMatrix(slot)
    }
    else if (slot < TileEngineeringTable.CRAFTING_OUTPUT_END)
    {
      return outputInventory(slot - TileEngineeringTable.CRAFTING_MATRIX_END)
    }
    else if (slot < TileEngineeringTable.PLAYER_OUTPUT_END && invPlayer != null)
    {
      return this.invPlayer.getStackInSlot(slot - TileEngineeringTable.CRAFTING_OUTPUT_END)
    }
    else if (searchInventories)
    {
      var idDisplacement: Int = TileEngineeringTable.PLAYER_OUTPUT_END
      for (dir <- ForgeDirection.VALID_DIRECTIONS)
      {
        val tile: TileEntity = toVectorWorld.add(dir).getTileEntity
        if (tile.isInstanceOf[IInventory])
        {
          val inventory: IInventory = tile.asInstanceOf[IInventory]
          val slotID: Int = slot - idDisplacement
          if (slotID >= 0 && slotID < inventory.getSizeInventory)
          {
            return inventory.getStackInSlot(slotID)
          }
          idDisplacement += inventory.getSizeInventory
        }
      }
    }
    return null
  }

  override def use(player: EntityPlayer, hitSide: Int, hit: Vector3): Boolean =
  {
    if (player.getCurrentEquippedItem != null && player.getCurrentEquippedItem.getItem.isInstanceOf[ItemHammer])
    {
      for (slot <- 0 to TileEngineeringTable.CRAFTING_OUTPUT_END)
      {
        val inputStack: ItemStack = getStackInSlot(slot)
        if (inputStack != null)
        {
          val oreName: String = OreDictionary.getOreName(OreDictionary.getOreID(inputStack))
          if (oreName != null && !(oreName == "Unknown"))
          {
            val outputs: Array[RecipeResource] = MachineRecipes.instance.getOutput(RecipeType.GRINDER.name, oreName)
            if (outputs != null && outputs.length > 0)
            {
              if (!world.isRemote && world.rand.nextFloat < 0.2)
              {
                for (resource <- outputs)
                {
                  val outputStack: ItemStack = resource.getItemStack.copy
                  if (outputStack != null)
                  {
                    InventoryUtility.dropItemStack(world, new Vector3(player), outputStack, 0)
                    setInventorySlotContents(slot, if (({
                      inputStack.stackSize -= 1;
                      inputStack.stackSize
                    }) <= 0) null
                    else inputStack)
                  }
                }
              }
              Electrodynamics.proxy.renderBlockParticle(world, new Vector3(x + 0.5, y + 0.5, z + 0.5), new Vector3((Math.random - 0.5f) * 3, (Math.random - 0.5f) * 3, (Math.random - 0.5f) * 3), Item.getIdFromItem(inputStack.getItem), 1)
              world.playSoundEffect(x + 0.5, y + 0.5, z + 0.5, Reference.prefix + "hammer", 0.5f, 0.8f + (0.2f * world.rand.nextFloat))
              player.addExhaustion(0.1f)
              player.getCurrentEquippedItem.damageItem(1, player)
              return true
            }
          }
        }
      }
      return true
    }
    if (hitSide == 1)
    {
      if (!world.isRemote)
      {
        val hitVector: Vector3 = new Vector3(hit.x, 0, hit.z)
        val regionLength: Double = 1d / 3d

        for (j <- 0 to 3)
        {
          for (k <- 0 to 3)
          {
            val check: Vector2 = new Vector2(j, k).multiply(regionLength)
            if (check.distance(hitVector.toVector2) < regionLength)
            {
              val slotID: Int = j * 3 + k
              interactCurrentItem(this, slotID, player)
              onInventoryChanged
              return true
            }
          }
        }
        onInventoryChanged
      }
      return true
    }
    else if (hitSide != 0)
    {
      if (!world.isRemote)
      {
        setPlayerInventory(player.inventory)
        var output: ItemStack = getStackInSlot(9)
        var firstLoop: Boolean = true
        while (output != null && (firstLoop || ControlKeyModifer.isControlDown(player)))
        {
          onPickUpFromSlot(player, 9, output)
          if (output.stackSize > 0)
          {
            InventoryUtility.dropItemStack(world, new Vector3(player), output, 0)
          }
          setInventorySlotContents(9, null)
          onInventoryChanged
          output = getStackInSlot(9)
          firstLoop = false
        }
        setPlayerInventory(null)
      }
      return true
    }
    return false
  }

  /**
   * Creates a "fake inventory" and hook the player up to the crafter to use the player's items.
   */
  def setPlayerInventory(invPlayer: InventoryPlayer)
  {
    if (searchInventories)
    {
      if (invPlayer != null)
      {
        playerSlots = new Array[Int](invPlayer.getSizeInventory)

        for (i <- 0 until playerSlots.length)
        {
          playerSlots(i) = i + TileEngineeringTable.CRAFTING_OUTPUT_END

        }
      }
      else
      {
        playerSlots = null
      }
      this.invPlayer = invPlayer
    }
  }

  def onPickUpFromSlot(entityPlayer: EntityPlayer, slotID: Int, itemStack: ItemStack)
  {
    if (!worldObj.isRemote)
    {
      if (itemStack != null)
      {
        val idealRecipeItem: Pair[ItemStack, Array[ItemStack]] = getCraftingManager.getIdealRecipe(itemStack)
        if (idealRecipeItem != null)
        {
          getCraftingManager.consumeItems(idealRecipeItem.right.clone: _*)
        }
        else
        {
          itemStack.stackSize = 0
        }
      }
    }
  }

  override def configure(player: EntityPlayer, side: Int, hit: Vector3): Boolean =
  {
    if (player.isSneaking)
    {
      searchInventories = !searchInventories
      if (!world.isRemote)
      {
        if (searchInventories)
        {
          player.addChatMessage(new ChatComponentText(LanguageUtility.getLocal("engineerTable.config.inventory.true")))
        }
        else
        {
          player.addChatMessage(new ChatComponentText(LanguageUtility.getLocal("engineerTable.config.inventory.false")))
        }
      }
      markUpdate
      return true
    }
    return super.configure(player, side, hit)
  }

  override def getDrops(metadata: Int, fortune: Int): ArrayList[ItemStack] = new ArrayList[ItemStack]

  override def onRemove(block: Block, par6: Int)
  {
    val stack: ItemStack = ItemBlockSaved.getItemStackWithNBT(block, world, xi, yi, zi)
    InventoryUtility.dropItemStack(world, center, stack)
  }

  override def canUpdate: Boolean =
  {
    return false
  }

  override def getDescriptionPacket: Packet =
  {
    val nbt: NBTTagCompound = new NBTTagCompound
    this.writeToNBT(nbt)
    return ResonantEngine.packetHandler.toMCPacket(new PacketTile(this, nbt))
  }

  /**
   * Writes a tile entity to NBT.
   */
  override def writeToNBT(nbt: NBTTagCompound)
  {
    super.writeToNBT(nbt)
    val nbtList: NBTTagList = new NBTTagList

    for (i <- 0 to this.getSizeInventory)
    {
      if (this.getStackInSlot(i) != null)
      {
        val var4: NBTTagCompound = new NBTTagCompound
        var4.setByte("Slot", i.asInstanceOf[Byte])
        this.getStackInSlot(i).writeToNBT(var4)
        nbtList.appendTag(var4)
      }
    }
    nbt.setTag("Items", nbtList)
    nbt.setBoolean("searchInventories", this.searchInventories)
  }

  def read(data: ByteBuf, player: EntityPlayer, `type`: PacketType)
  {
    try
    {
      readFromNBT(ByteBufUtils.readTag(data))
    }
    catch
      {
        case e: Exception =>
        {
          e.printStackTrace
        }
      }
  }

  /**
   * NBT Data
   */
  override def readFromNBT(nbt: NBTTagCompound)
  {
    super.readFromNBT(nbt)
    val nbtList: NBTTagList = nbt.getTagList("Items", 0)
    this.craftingMatrix = new Array[ItemStack](9)
    this.outputInventory = new Array[ItemStack](1)

    for (i <- 0 to nbtList.tagCount)
    {
      val stackTag: NBTTagCompound = nbtList.getCompoundTagAt(i)
      val id: Byte = stackTag.getByte("Slot")
      if (id >= 0 && id < this.getSizeInventory)
      {
        this.setInventorySlotContents(id, ItemStack.loadItemStackFromNBT(stackTag))
      }
    }

    this.searchInventories = nbt.getBoolean("searchInventories")
  }

  override def decrStackSize(i: Int, amount: Int): ItemStack =
  {
    if (getStackInSlot(i) != null)
    {
      var stack: ItemStack = null
      if (getStackInSlot(i).stackSize <= amount)
      {
        stack = getStackInSlot(i)
        setInventorySlotContents(i, null)
        return stack
      }
      else
      {
        stack = getStackInSlot(i).splitStack(amount)
        if (getStackInSlot(i).stackSize == 0)
        {
          setInventorySlotContents(i, null)
        }
        return stack
      }
    }
    else
    {
      return null
    }
  }

  /**
   * When some containers are closed they call this on each slot, then drop whatever it returns as
   * an EntityItem - like when you close a workbench GUI.
   */
  override def getStackInSlotOnClosing(slot: Int): ItemStack =
  {
    if (this.getStackInSlot(slot) != null)
    {
      val var2: ItemStack = this.getStackInSlot(slot)
      this.setInventorySlotContents(slot, null)
      return var2
    }
    else
    {
      return null
    }
  }

  override def isItemValidForSlot(i: Int, itemstack: ItemStack): Boolean =
  {
    return true
  }

  override def getInventoryStackLimit: Int =
  {
    return 64
  }

  override def isUseableByPlayer(entityplayer: EntityPlayer): Boolean =
  {
    return if (this.worldObj.getTileEntity(this.xCoord, this.yCoord, this.zCoord) ne this) false else entityplayer.getDistanceSq(this.xCoord + 0.5D, this.yCoord + 0.5D, this.zCoord + 0.5D) <= 64.0D
  }

  override def getAccessibleSlotsFromSide(side: Int): Array[Int] =
  {
    return new Array[Int](0)
  }

  /**
   * Auto-crafting methods.
   */
  override def canInsertItem(slot: Int, itemstack: ItemStack, side: Int): Boolean =
  {
    if (getStackInSlot(4) != null && getStackInSlot(4).getItem.isInstanceOf[ItemImprint])
    {
      return true
    }
    var minSize: Int = 64
    var optimalSlot: Int = -1

    for (i <- 0 to craftingMatrix.length)
    {
      val checkStack: ItemStack = getStackInSlot(i)
      if (checkStack != null && checkStack.isItemEqual(itemstack))
      {
        if (checkStack.stackSize < minSize || optimalSlot < 0)
        {
          optimalSlot = i
          minSize = checkStack.stackSize
        }
      }
    }
    return slot == optimalSlot
  }

  override def canExtractItem(slot: Int, itemstack: ItemStack, side: Int): Boolean =
  {
    val outputStack: ItemStack = getStackInSlot(TileEngineeringTable.CRAFTING_MATRIX_END)
    if (outputStack != null)
    {
      val idealRecipeItem: Pair[ItemStack, Array[ItemStack]] = this.getCraftingManager.getIdealRecipe(outputStack)
      val doubleResults: Array[ItemStack] = ArrayUtils.addAll(idealRecipeItem.right, idealRecipeItem.right: _*)
      if (!getCraftingManager.consumeItems(false, doubleResults: _*))
      {
        return false
      }
    }
    return slot == TileEngineeringTable.CRAFTING_MATRIX_END
  }

  def getCraftingInv: Array[Int] =
  {
    var slots: Array[Int] = TileEngineeringTable.craftingSlots
    if (playerSlots != null)
    {
      slots = ArrayUtils.addAll(playerSlots, slots: _*)
    }
    if (searchInventories)
    {
      var temporaryInvID: Int = TileEngineeringTable.PLAYER_OUTPUT_END
      for (dir <- ForgeDirection.VALID_DIRECTIONS)
      {
        val tile: TileEntity = toVectorWorld.add(dir).getTileEntity(worldObj)
        if (tile.isInstanceOf[IInventory])
        {
          val inventory: IInventory = tile.asInstanceOf[IInventory]
          val nearbySlots: Array[Int] = new Array[Int](inventory.getSizeInventory)

          for (i <- 0 to inventory.getSizeInventory)
          {
            temporaryInvID = temporaryInvID + 1;
            nearbySlots(i) = temporaryInvID;

          }
          slots = ArrayUtils.addAll(nearbySlots, slots: _*)
        }
      }
    }
    return slots
  }

  override def renderDynamic(position: Vector3, frame: Float, pass: Int)
  {
    GL11.glPushMatrix
    RenderItemOverlayUtility.renderItemOnSides(TileEngineeringTable.this, getStackInSlot(9), position.x, position.y, position.z)
    RenderItemOverlayUtility.renderTopOverlay(TileEngineeringTable.this, craftingMatrix, getDirection, position.x, position.y - 0.1, position.z)
    GL11.glPopMatrix
  }

  override def getDirection: ForgeDirection =
  {
    return null
  }

  override def setDirection(direction: ForgeDirection)
  {
  }
}