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._
import li.cil.oc.api.driver.DeviceInfo.DeviceAttribute
import li.cil.oc.api.driver.DeviceInfo.DeviceClass
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.Analyzable
import li.cil.oc.api.network._
import li.cil.oc.common.SaveHandler
import li.cil.oc.integration.util.Waila
import li.cil.oc.server.{PacketSender => ServerPacketSender}
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.nbt.NBTTagCompound
import net.minecraft.util.AxisAlignedBB
import net.minecraft.util.Vec3
import net.minecraftforge.common.util.ForgeDirection

import scala.collection.convert.WrapAsJava._
import scala.collection.mutable

class Hologram(var tier: Int) extends traits.Environment with SidedEnvironment with Analyzable with traits.Rotatable with DeviceInfo {
  def this() = this(0)

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

  final val width = 3 * 16

  final val height = 2 * 16 // 32 bit in an int

  private final lazy val deviceInfo = Map(
    DeviceAttribute.Class -> DeviceClass.Display,
    DeviceAttribute.Description -> "Holographic projector",
    DeviceAttribute.Vendor -> Constants.DeviceInfo.DefaultVendor,
    DeviceAttribute.Product -> ("VirtualViewer H1-" + (tier + 1).toString),
    DeviceAttribute.Capacity -> (width * width * height).toString,
    DeviceAttribute.Width -> colors.length.toString
  )

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

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

  // Layout is: first half is lower bit, second half is higher bit for the
  // voxels in the cube. This is to retain compatibility with pre 1.3 saves.
  val volume = new Array[Int](width * width * 2)

  // Render scale.
  var scale = 1.0

  // Projection Y position offset - consider adding X,Z later perhaps
  var translation = Vec3.createVectorHelper(0, 0, 0)

  // Relative number of lit columns (for energy cost).
  var litRatio = -1.0

  // Whether we need to recompile our display list.
  var needsRendering = false

  // Store it here for convenience, this is the number of visible voxel faces
  // as determined in the last VBO index update. See HologramRenderer.
  var visibleQuads = 0

  // What parts of the hologram changed and need an update packet.
  var dirty = mutable.Set.empty[Short]

  // Interval of dirty columns.
  var dirtyFromX = Int.MaxValue
  var dirtyUntilX = -1
  var dirtyFromZ = Int.MaxValue
  var dirtyUntilZ = -1

  var hasPower = true

  // Rotation base state. Current rotation is based on world time. See HologramRenderer.
  var rotationAngle = 0f
  var rotationX = 0f
  var rotationY = 0f
  var rotationZ = 0f
  var rotationSpeed = 0f
  var rotationSpeedX = 0f
  var rotationSpeedY = 0f
  var rotationSpeedZ = 0f

  final val colorsByTier = Array(Array(0x00FF00), Array(0x0000FF, 0x00FF00, 0xFF0000)) // 0xBBGGRR for rendering convenience

  // This is a def and not a val for loading (where the tier comes from the nbt and is always 0 here).
  def colors = colorsByTier(tier)

  def getColor(x: Int, y: Int, z: Int) = {
    val lbit = (volume(x + z * width) >>> y) & 1
    val hbit = (volume(x + z * width + width * width) >>> y) & 1
    lbit | (hbit << 1)
  }

  def setColor(x: Int, y: Int, z: Int, value: Int) {
    if ((value & 3) != getColor(x, y, z)) {
      val lbit = value & 1
      val hbit = (value >>> 1) & 1
      volume(x + z * width) = (volume(x + z * width) & ~(1 << y)) | (lbit << y)
      volume(x + z * width + width * width) = (volume(x + z * width + width * width) & ~(1 << y)) | (hbit << y)
      setDirty(x, z)
    }
  }

  private def setDirty(x: Int, z: Int) {
    dirty += ((x.toByte << 8) | z.toByte).toShort
    dirtyFromX = math.min(dirtyFromX, x)
    dirtyUntilX = math.max(dirtyUntilX, x + 1)
    dirtyFromZ = math.min(dirtyFromZ, z)
    dirtyUntilZ = math.max(dirtyUntilZ, z + 1)
    litRatio = -1
  }

  private def resetDirtyFlag() {
    dirty.clear()
    dirtyFromX = Int.MaxValue
    dirtyUntilX = -1
    dirtyFromZ = Int.MaxValue
    dirtyUntilZ = -1
  }

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

  @SideOnly(Side.CLIENT)
  override def canConnect(side: ForgeDirection) = toLocal(side) == ForgeDirection.DOWN

  override def sidedNode(side: ForgeDirection) = if (toLocal(side) == ForgeDirection.DOWN) node else null

  // Override automatic analyzer implementation for sided environments.
  override def onAnalyze(player: EntityPlayer, side: Int, hitX: Float, hitY: Float, hitZ: Float) = Array(node)

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

  @Callback(doc = """function() -- Clears the hologram.""")
  def clear(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
    for (i <- volume.indices) volume(i) = 0
    ServerPacketSender.sendHologramClear(this)
    resetDirtyFlag()
    litRatio = 0
    null
  }

  @Callback(direct = true, doc = """function(x:number, y:number, z:number):number -- Returns the value for the specified voxel.""")
  def get(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
    val (x, y, z) = checkCoordinates(args)
    result(getColor(x, y, z))
  }

  @Callback(direct = true, limit = 256, doc = """function(x:number, y:number, z:number, value:number or boolean) -- Set the value for the specified voxel.""")
  def set(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
    val (x, y, z) = checkCoordinates(args)
    val value = checkColor(args, 3)
    setColor(x, y, z, value)
    null
  }

  @Callback(direct = true, limit = 128, doc = """function(x:number, z:number[, minY:number], maxY:number, value:number or boolean) -- Fills an interval of a column with the specified value.""")
  def fill(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
    val (x, _, z) = checkCoordinates(args, 0, -1, 1)
    val (minY, maxY, value) =
      if (args.count > 4)
        (math.min(32, math.max(1, args.checkInteger(2))), math.min(32, math.max(1, args.checkInteger(3))), checkColor(args, 4))
      else
        (1, math.min(32, math.max(1, args.checkInteger(2))), checkColor(args, 3))
    if (minY > maxY) throw new IllegalArgumentException("interval is empty")

    val mask = (0xFFFFFFFF >>> (31 - (maxY - minY))) << (minY - 1)
    val lbit = value & 1
    val hbit = (value >>> 1) & 1
    if (lbit == 0 || height == 0) volume(x + z * width) &= ~mask
    else volume(x + z * width) |= mask
    if (hbit == 0 || height == 0) volume(x + z * width + width * width) &= ~mask
    else volume(x + z * width + width * width) |= mask

    setDirty(x, z)
    null
  }

  @Callback(doc = """function(data:string) -- Set the raw buffer to the specified byte array, where each byte represents a voxel color. Nesting is x,z,y.""")
  def setRaw(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
    val data = args.checkByteArray(0)
    for (x <- 0 until width; z <- 0 until width) {
      val offset = z * height + x * height * width
      if (data.length >= offset + height) {
        var lbit = 0
        var hbit = 0
        for (y <- (height - 1) to 0 by -1) {
          val color = data(offset + y)
          lbit |= (color & 1) << y
          hbit |= ((color & 3) >>> 1) << y
        }
        val index = x + z * width
        if (volume(index) != lbit || volume(index + width * width) != hbit) {
          volume(index) = lbit
          volume(index + width * width) = hbit
          setDirty(x, z)
        }
      }
    }
    context.pause(Settings.get.hologramSetRawDelay)
    null
  }

  @Callback(doc = """function(x:number, z:number, sx:number, sz:number, tx:number, tz:number) -- Copies an area of columns by the specified translation.""")
  def copy(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
    val (x, _, z) = checkCoordinates(args, 0, -1, 1)
    val w = args.checkInteger(2)
    val h = args.checkInteger(3)
    val tx = args.checkInteger(4)
    val tz = args.checkInteger(5)

    // Anything to do at all?
    if (w <= 0 || h <= 0) return null
    if (tx == 0 && tz == 0) return null
    // Loop over the target rectangle, starting from the directions away from
    // the source rectangle and copy the data. This way we ensure we don't
    // overwrite anything we still need to copy.
    val (dx0, dx1) = (math.max(0, math.min(width - 1, x + tx + w - 1)), math.max(0, math.min(width, x + tx))) match {
      case dx if tx > 0 => dx
      case dx => dx.swap
    }
    val (dz0, dz1) = (math.max(0, math.min(width - 1, z + tz + h - 1)), math.max(0, math.min(width, z + tz))) match {
      case dz if tz > 0 => dz
      case dz => dz.swap
    }
    val (sx, sz) = (if (tx > 0) -1 else 1, if (tz > 0) -1 else 1)
    // Copy values to destination rectangle if there source is valid.
    for (nz <- dz0 to dz1 by sz) {
      nz - tz match {
        case oz if oz >= 0 && oz < width =>
          for (nx <- dx0 to dx1 by sx) {
            nx - tx match {
              case ox if ox >= 0 && ox < width =>
                volume(nz * width + nx) = volume(oz * width + ox)
                volume(nz * width + nx + width * width) = volume(oz * width + ox + width * width)
              case _ => /* Got no source column. */
            }
          }
        case _ => /* Got no source row. */
      }
    }

    // Mark target rectangle dirty.
    setDirty(math.min(dx0, dx1), math.min(dz0, dz1))
    setDirty(math.max(dx0, dx1), math.max(dz0, dz1))

    // The reasoning here is: it'd take 18 ticks to do the whole are with fills,
    // so make this slightly more efficient (15 ticks - 0.75 seconds). Make it
    // 'free' if it's less than 0.25 seconds, i.e. for small copies.
    val area = (math.max(dx0, dx1) - math.min(dx0, dx1)) * (math.max(dz0, dz1) - math.min(dz0, dz1))
    val relativeArea = math.max(0, area / (width * width).toFloat - 0.25)
    context.pause(relativeArea)

    null
  }

  @Callback(direct = true, doc = """function():number -- Returns the render scale of the hologram.""")
  def getScale(context: Context, args: Arguments): Array[AnyRef] = {
    result(scale)
  }

  @Callback(doc = """function(value:number) -- Set the render scale. A larger scale consumes more energy.""")
  def setScale(context: Context, args: Arguments): Array[AnyRef] = {
    scale = math.max(0.333333, math.min(Settings.get.hologramMaxScaleByTier(tier), args.checkDouble(0)))
    ServerPacketSender.sendHologramScale(this)
    null
  }

  @Callback(direct = true, doc = """function():number, number, number -- Returns the relative render projection offsets of the hologram.""")
  def getTranslation(context: Context, args: Arguments): Array[AnyRef] = {
    result(translation.xCoord, translation.yCoord, translation.zCoord)
  }

  @Callback(doc = """function(tx:number, ty:number, tz:number) -- Sets the relative render projection offsets of the hologram.""")
  def setTranslation(context: Context, args: Arguments): Array[AnyRef] = {
    // Validate all axes before setting the values.
    val maxTranslation = Settings.get.hologramMaxTranslationByTier(tier)
    val tx = math.max(-maxTranslation, math.min(maxTranslation, args.checkDouble(0)))
    val ty = math.max(0, math.min(maxTranslation * 2, args.checkDouble(1)))
    val tz = math.max(-maxTranslation, math.min(maxTranslation, args.checkDouble(2)))

    translation.xCoord = tx
    translation.yCoord = ty
    translation.zCoord = tz

    ServerPacketSender.sendHologramOffset(this)
    null
  }

  @Callback(direct = true, doc = """function():number -- The color depth supported by the hologram.""")
  def maxDepth(context: Context, args: Arguments): Array[AnyRef] = {
    result(tier + 1)
  }

  @Callback(doc = """function(index:number):number -- Get the color defined for the specified value.""")
  def getPaletteColor(context: Context, args: Arguments): Array[AnyRef] = {
    val index = args.checkInteger(0)
    if (index < 1 || index > colors.length) throw new ArrayIndexOutOfBoundsException()
    // Colors are stored as 0xAABBGGRR for rendering convenience, so convert them.
    result(convertColor(colors(index - 1)))
  }

  @Callback(doc = """function(index:number, value:number):number -- Set the color defined for the specified value.""")
  def setPaletteColor(context: Context, args: Arguments): Array[AnyRef] = {
    val index = args.checkInteger(0)
    if (index < 1 || index > colors.length) throw new ArrayIndexOutOfBoundsException()
    val value = args.checkInteger(1)
    val oldValue = colors(index - 1)
    // Change byte order here to allow passing stored color to OpenGL "as-is"
    // (as whole Int, i.e. 0xAABBGGRR, alpha is unused but present for alignment)
    colors(index - 1) = convertColor(value)
    ServerPacketSender.sendHologramColor(this, index - 1, colors(index - 1))
    result(oldValue)
  }

  @Callback(doc = """function(angle:number, x:number, y:number, z:number):boolean -- Set the base rotation of the displayed hologram.""")
  def setRotation(context: Context, args: Arguments): Array[AnyRef] = {
    if (tier > 0) {
      val r = args.checkDouble(0) % 360
      val x = args.checkDouble(1)
      val y = args.checkDouble(2)
      val z = args.checkDouble(3)

      rotationAngle = r.toFloat
      rotationX = x.toFloat
      rotationY = y.toFloat
      rotationZ = z.toFloat
      ServerPacketSender.sendHologramRotation(this)

      result(true)
    }
    else result(Unit, "not supported")
  }

  @Callback(doc = """function(speed:number, x:number, y:number, z:number):boolean -- Set the rotation speed of the displayed hologram.""")
  def setRotationSpeed(context: Context, args: Arguments): Array[AnyRef] = {
    if (tier > 0) {
      val v = args.checkDouble(0) max -360 * 4 min 360 * 4
      val x = args.checkDouble(1)
      val y = args.checkDouble(2)
      val z = args.checkDouble(3)

      rotationSpeed = v.toFloat
      rotationSpeedX = x.toFloat
      rotationSpeedY = y.toFloat
      rotationSpeedZ = z.toFloat
      ServerPacketSender.sendHologramRotationSpeed(this)

      result(true)
    }
    else result(Unit, "not supported")
  }

  private def checkCoordinates(args: Arguments, idxX: Int = 0, idxY: Int = 1, idxZ: Int = 2) = {
    val x = if (idxX >= 0) args.checkInteger(idxX) - 1 else 0
    if (x < 0 || x >= width) throw new ArrayIndexOutOfBoundsException("x")
    val y = if (idxY >= 0) args.checkInteger(idxY) - 1 else 0
    if (y < 0 || y >= height) throw new ArrayIndexOutOfBoundsException("y")
    val z = if (idxZ >= 0) args.checkInteger(idxZ) - 1 else 0
    if (z < 0 || z >= width) throw new ArrayIndexOutOfBoundsException("z")
    (x, y, z)
  }

  private def checkColor(args: Arguments, index: Int) = {
    val value =
      if (args.isBoolean(index))
        if (args.checkBoolean(index)) 1 else 0
      else
        args.checkInteger(index)
    if (value < 0 || value > colors.length) throw new IllegalArgumentException("invalid value")
    value
  }

  private def convertColor(color: Int) = {
    ((color & 0x0000FF) << 16) | (color & 0x00FF00) | ((color & 0xFF0000) >>> 16)
  }

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

  override def canUpdate = isServer

  override def updateEntity() {
    super.updateEntity()
    if (isServer) {
      if (dirty.nonEmpty) this.synchronized {
        val dirtySizeX = dirtyUntilX - dirtyFromX
        val dirtySizeZ = dirtyUntilZ - dirtyFromZ
        // Sending the dirty area requires
        //   dirtySizeX * dirtySizeZ * (4 + 4)
        // bytes (2 = low + high byte).
        // Sending a single changes requires
        //   changes * (4 + 4 + 2)
        // bytes (other 2 byte = coords).
        // So at some point it'll be cheaper to just send the area:
        // changes * (4 + 4 + 2) = dirtySizeX * dirtySizeZ * (4 + 4)
        // changes = dirtySizeX * dirtySizeZ * (4 + 4) / (4 + 4 + 2) = dirtySizeX * dirtySizeZ * 0.8
        // So if changes are larger than that, just send the full hologram.
        if (dirty.size > dirtySizeX * dirtySizeZ * 0.8)
          ServerPacketSender.sendHologramArea(this)
        else
          ServerPacketSender.sendHologramValues(this)
        resetDirtyFlag()
      }
      if (world.getTotalWorldTime % Settings.get.tickFrequency == 0) {
        if (litRatio < 0) this.synchronized {
          litRatio = 0
          for (i <- volume.indices) {
            if (volume(i) != 0) litRatio += 1
          }
          litRatio /= volume.length
        }

        val hadPower = hasPower
        val neededPower = Settings.get.hologramCost * litRatio * scale * Settings.get.tickFrequency
        hasPower = node.tryChangeBuffer(-neededPower)
        if (hasPower != hadPower) {
          ServerPacketSender.sendHologramPowerChange(this)
        }
      }
    }
  }

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

  override def shouldRenderInPass(pass: Int) = pass == 1

  override def getMaxRenderDistanceSquared = scale / Settings.get.hologramMaxScaleByTier.max * Settings.get.hologramRenderDistance * Settings.get.hologramRenderDistance

  def getFadeStartDistanceSquared = scale / Settings.get.hologramMaxScaleByTier.max * Settings.get.hologramFadeStartDistance * Settings.get.hologramFadeStartDistance

  private final val Sqrt2 = Math.sqrt(2)
  override def getRenderBoundingBox = {
    val cx = x + 0.5
    val cy = y + 0.5
    val cz = z + 0.5
    val sh = width / 16 * scale * Sqrt2 // overscale to take into account 45 degree rotation
    val sv = height / 16 * scale * Sqrt2
    AxisAlignedBB.getBoundingBox(
      cx + (-0.5 + translation.xCoord) * sh,
      cy + translation.yCoord * sv,
      cz + (-0.5 + translation.zCoord) * sh,
      cx + (0.5 + translation.xCoord) * sh,
      cy + (1 + translation.yCoord) * sv,
      cz + (0.5 + translation.xCoord) * sh)
  }

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

  override def readFromNBTForServer(nbt: NBTTagCompound) {
    tier = nbt.getByte(Settings.namespace + "tier") max 0 min 1
    super.readFromNBTForServer(nbt)
    val tag = SaveHandler.loadNBT(nbt, node.address + "_data")
    tag.getIntArray("volume").copyToArray(volume)
    tag.getIntArray("colors").map(convertColor).copyToArray(colors)
    scale = nbt.getDouble(Settings.namespace + "scale")
    translation.xCoord = nbt.getDouble(Settings.namespace + "offsetX")
    translation.yCoord = nbt.getDouble(Settings.namespace + "offsetY")
    translation.zCoord = nbt.getDouble(Settings.namespace + "offsetZ")
    rotationAngle = nbt.getFloat(Settings.namespace + "rotationAngle")
    rotationX = nbt.getFloat(Settings.namespace + "rotationX")
    rotationY = nbt.getFloat(Settings.namespace + "rotationY")
    rotationZ = nbt.getFloat(Settings.namespace + "rotationZ")
    rotationSpeed = nbt.getFloat(Settings.namespace + "rotationSpeed")
    rotationSpeedX = nbt.getFloat(Settings.namespace + "rotationSpeedX")
    rotationSpeedY = nbt.getFloat(Settings.namespace + "rotationSpeedY")
    rotationSpeedZ = nbt.getFloat(Settings.namespace + "rotationSpeedZ")
  }

  override def writeToNBTForServer(nbt: NBTTagCompound) = this.synchronized {
    nbt.setByte(Settings.namespace + "tier", tier.toByte)
    super.writeToNBTForServer(nbt)
    if (!Waila.isSavingForTooltip) {
      SaveHandler.scheduleSave(world, x, z, nbt, node.address + "_data", tag => {
        tag.setIntArray("volume", volume)
        tag.setIntArray("colors", colors.map(convertColor))
      })
    }
    nbt.setDouble(Settings.namespace + "scale", scale)
    nbt.setDouble(Settings.namespace + "offsetX", translation.xCoord)
    nbt.setDouble(Settings.namespace + "offsetY", translation.yCoord)
    nbt.setDouble(Settings.namespace + "offsetZ", translation.zCoord)
    nbt.setFloat(Settings.namespace + "rotationAngle", rotationAngle)
    nbt.setFloat(Settings.namespace + "rotationX", rotationX)
    nbt.setFloat(Settings.namespace + "rotationY", rotationY)
    nbt.setFloat(Settings.namespace + "rotationZ", rotationZ)
    nbt.setFloat(Settings.namespace + "rotationSpeed", rotationSpeed)
    nbt.setFloat(Settings.namespace + "rotationSpeedX", rotationSpeedX)
    nbt.setFloat(Settings.namespace + "rotationSpeedY", rotationSpeedY)
    nbt.setFloat(Settings.namespace + "rotationSpeedZ", rotationSpeedZ)
  }

  @SideOnly(Side.CLIENT)
  override def readFromNBTForClient(nbt: NBTTagCompound) {
    super.readFromNBTForClient(nbt)
    nbt.getIntArray("volume").copyToArray(volume)
    nbt.getIntArray("colors").copyToArray(colors)
    scale = nbt.getDouble("scale")
    hasPower = nbt.getBoolean("hasPower")
    translation.xCoord = nbt.getDouble("offsetX")
    translation.yCoord = nbt.getDouble("offsetY")
    translation.zCoord = nbt.getDouble("offsetZ")
    rotationAngle = nbt.getFloat("rotationAngle")
    rotationX = nbt.getFloat("rotationX")
    rotationY = nbt.getFloat("rotationY")
    rotationZ = nbt.getFloat("rotationZ")
    rotationSpeed = nbt.getFloat("rotationSpeed")
    rotationSpeedX = nbt.getFloat("rotationSpeedX")
    rotationSpeedY = nbt.getFloat("rotationSpeedY")
    rotationSpeedZ = nbt.getFloat("rotationSpeedZ")
  }

  override def writeToNBTForClient(nbt: NBTTagCompound) {
    super.writeToNBTForClient(nbt)
    nbt.setIntArray("volume", volume)
    nbt.setIntArray("colors", colors)
    nbt.setDouble("scale", scale)
    nbt.setBoolean("hasPower", hasPower)
    nbt.setDouble("offsetX", translation.xCoord)
    nbt.setDouble("offsetY", translation.yCoord)
    nbt.setDouble("offsetZ", translation.zCoord)
    nbt.setFloat("rotationAngle", rotationAngle)
    nbt.setFloat("rotationX", rotationX)
    nbt.setFloat("rotationY", rotationY)
    nbt.setFloat("rotationZ", rotationZ)
    nbt.setFloat("rotationSpeed", rotationSpeed)
    nbt.setFloat("rotationSpeedX", rotationSpeedX)
    nbt.setFloat("rotationSpeedY", rotationSpeedY)
    nbt.setFloat("rotationSpeedZ", rotationSpeedZ)
  }
}
