package li.cil.oc.common.nanomachines

import java.lang
import java.util.UUID

import com.google.common.base.Charsets
import com.google.common.base.Strings
import li.cil.oc.Constants
import li.cil.oc.Settings
import li.cil.oc.api
import li.cil.oc.api.nanomachines.Behavior
import li.cil.oc.api.nanomachines.Controller
import li.cil.oc.api.nanomachines.DisableReason
import li.cil.oc.api.network.Packet
import li.cil.oc.api.network.WirelessEndpoint
import li.cil.oc.common.item.data.NanomachineData
import li.cil.oc.integration.util.DamageSourceWithRandomCause
import li.cil.oc.server.PacketSender
import li.cil.oc.util.BlockPosition
import li.cil.oc.util.ExtendedNBT._
import li.cil.oc.util.InventoryUtils
import li.cil.oc.util.PlayerUtils
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.entity.player.EntityPlayerMP
import net.minecraft.nbt.NBTTagCompound
import net.minecraft.potion.Potion
import net.minecraft.potion.PotionEffect
import net.minecraft.world.World

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

class ControllerImpl(val player: EntityPlayer) extends Controller with WirelessEndpoint {
  if (isServer) api.Network.joinWirelessNetwork(this)
  var previousDimension = player.worldObj.provider.dimensionId

  lazy val CommandRange = Settings.get.nanomachinesCommandRange * Settings.get.nanomachinesCommandRange
  final val FullSyncInterval = 20 * 60

  final val OverloadDamage = new DamageSourceWithRandomCause("oc.nanomachinesOverload", 3).
    setDamageBypassesArmor().
    setDamageIsAbsolute()

  var uuid = UUID.randomUUID.toString
  var responsePort = 0
  var commandDelay = 0
  var queuedCommand: Option[() => Unit] = None
  var storedEnergy = Settings.get.bufferNanomachines * 0.25
  var hadPower = true
  val configuration = new NeuralNetwork(this)
  val activeBehaviors = mutable.Set.empty[Behavior]
  var activeBehaviorsDirty = true
  var hasSentConfiguration = false

  override def world: World = player.getEntityWorld

  override def x: Int = BlockPosition(player).x

  override def y: Int = BlockPosition(player).y

  override def z: Int = BlockPosition(player).z

  override def receivePacket(packet: Packet, sender: WirelessEndpoint): Unit = {
    if (getLocalBuffer > 0 && commandDelay < 1 && !player.isDead) {
      val (dx, dy, dz) = ((sender.x + 0.5) - player.posX, (sender.y + 0.5) - player.posY, (sender.z + 0.5) - player.posZ)
      val dSquared = dx * dx + dy * dy + dz * dz
      if (dSquared <= CommandRange) packet.data.headOption match {
        case Some(header: Array[Byte]) if new String(header, Charsets.UTF_8) == "nanomachines" =>
          val command = packet.data.drop(1).map {
            case value: Array[Byte] => new String(value, Charsets.UTF_8)
            case value => value
          }
          command match {
            case Array("setResponsePort", port: java.lang.Number) =>
              responsePort = port.intValue max 0 min 0xFFFF
              respond(sender, "port", responsePort)
            case Array("getPowerState") =>
              respond(sender, "power", getLocalBuffer, getLocalBufferSize)
            case Array("saveConfiguration") =>
              val nanomachines = api.Items.get(Constants.ItemName.Nanomachines)
              try {
                val index = player.inventory.mainInventory.indexWhere(stack => api.Items.get(stack) == nanomachines && new NanomachineData(stack).configuration.isEmpty)
                if (index >= 0) {
                  val stack = player.inventory.decrStackSize(index, 1)
                  new NanomachineData(this).save(stack)
                  player.inventory.addItemStackToInventory(stack)
                  InventoryUtils.spawnStackInWorld(BlockPosition(player), stack)
                  respond(sender, "saved", true)
                }
                else respond(sender, "saved", false, "no nanomachines")
              }
              catch {
                case _: Throwable =>
                  respond(sender, "saved", false, "error")
              }
            case Array("getHealth") =>
              respond(sender, "health", player.getHealth, player.getMaxHealth)
            case Array("getHunger") =>
              respond(sender, "hunger", player.getFoodStats.getFoodLevel, player.getFoodStats.getSaturationLevel)
            case Array("getAge") =>
              respond(sender, "age", (player.getAge / 20f).toInt)
            case Array("getName") =>
              respond(sender, "name", player.getDisplayName)
            case Array("getExperience") =>
              respond(sender, "experience", player.experienceLevel)

            case Array("getTotalInputCount") =>
              respond(sender, "totalInputCount", getTotalInputCount)
            case Array("getSafeActiveInputs") =>
              respond(sender, "safeActiveInputs", getSafeActiveInputs)
            case Array("getMaxActiveInputs") =>
              respond(sender, "maxActiveInputs", getMaxActiveInputs)
            case Array("getInput", index: java.lang.Number) =>
              try {
                val trigger = getInput(index.intValue - 1)
                respond(sender, "input", index.intValue, trigger)
              }
              catch {
                case _: Throwable =>
                  respond(sender, "input", "error")
              }
            case Array("setInput", index: java.lang.Number, value: java.lang.Boolean) =>
              try {
                if (setInput(index.intValue - 1, value.booleanValue)) {
                  respond(sender, "input", index.intValue, getInput(index.intValue - 1))
                }
                else {
                  respond(sender, "input", "too many active inputs")
                }
              }
              catch {
                case _: Throwable =>
                  respond(sender, "input", "error")
              }
            case Array("getActiveEffects") =>
              configuration.synchronized {
                val names = getActiveBehaviors.map(_.getNameHint).filterNot(Strings.isNullOrEmpty)
                val joined = "{" + names.map(_.replace(',', '_').replace('"', '_')).mkString(",") + "}"
                respond(sender, "effects", joined)
              }
            case _ => // Ignore.
          }
        case _ => // Not for us.
      }
    }
  }

  def respond(endpoint: WirelessEndpoint, data: Any*): Unit = {
    queuedCommand = Option(() => {
      if (responsePort > 0) {
        val cost = Settings.get.wirelessCostPerRange * CommandRange
        val epsilon = 0.1
        if (changeBuffer(-cost) > -epsilon) {
          val packet = api.Network.newPacket(uuid, null, responsePort, (Iterable("nanomachines") ++ data.map(_.asInstanceOf[AnyRef])).toArray)
          api.Network.sendWirelessPacket(this, CommandRange, packet)
        }
      }
    })
    commandDelay = (Settings.get.nanomachinesCommandDelay * 20).toInt
  }

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

  override def reconfigure() = {
    if (isServer) configuration.synchronized {
      configuration.reconfigure()
      activeBehaviorsDirty = true

      player match {
        case playerMP: EntityPlayerMP if playerMP.playerNetServerHandler != null =>
          player.addPotionEffect(new PotionEffect(Potion.blindness.id, 100))
          player.addPotionEffect(new PotionEffect(Potion.poison.id, 150))
          player.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, 200))
          changeBuffer(-Settings.get.nanomachineReconfigureCost)

          hasSentConfiguration = false
        case _ => // We're still setting up / loading.
      }
    }
    this
  }

  override def getTotalInputCount: Int = configuration.synchronized(configuration.triggers.length)

  override def getSafeActiveInputs: Int = Settings.get.nanomachinesSafeInputsActive

  override def getMaxActiveInputs: Int = Settings.get.nanomachinesMaxInputsActive

  override def getInput(index: Int): Boolean = configuration.synchronized(configuration.triggers(index).isActive)

  override def setInput(index: Int, value: Boolean): Boolean = {
    isServer && configuration.synchronized {
      (!value || configuration.triggers.count(_.isActive) < Settings.get.nanomachinesMaxInputsActive) && {
        configuration.triggers(index).isActive = value
        activeBehaviorsDirty = true
        true
      }
    }
  }

  override def getActiveBehaviors: lang.Iterable[Behavior] = configuration.synchronized {
    cleanActiveBehaviors(DisableReason.InputChanged)
    activeBehaviors
  }

  override def getInputCount(behavior: Behavior): Int = configuration.synchronized(configuration.inputs(behavior))

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

  override def getLocalBuffer: Double = storedEnergy

  override def getLocalBufferSize: Double = Settings.get.bufferNanomachines

  override def changeBuffer(delta: Double): Double = {
    if (isClient) delta
    else if (delta < 0 && (Settings.get.ignorePower || player.capabilities.isCreativeMode)) 0.0
    else {
      val newValue = storedEnergy + delta
      storedEnergy = math.min(math.max(newValue, 0), getLocalBufferSize)
      newValue - storedEnergy
    }
  }

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

  def update(): Unit = {
    if (player.isDead) {
      return
    }

    if (isServer) {
      if (commandDelay > 0) {
        commandDelay -= 1
        if (commandDelay == 0) {
          queuedCommand.foreach(_ ())
          queuedCommand = None
        }
      }

      // Handle dimension changes, the robust way (because when logging in,
      // load is called while the world is still set to the overworld, but
      // no dimension change event is fired if the player actually logged
      // out in another dimension... yay)
      if (player.worldObj.provider.dimensionId != previousDimension) {
        api.Network.leaveWirelessNetwork(this, previousDimension)
        api.Network.joinWirelessNetwork(this)
        previousDimension = player.worldObj.provider.dimensionId
      }
      else {
        api.Network.updateWirelessNetwork(this)
      }
    }

    var hasPower = getLocalBuffer > 0 || Settings.get.ignorePower
    lazy val active = getActiveBehaviors.toIterable // Wrap once.
    lazy val activeInputs = configuration.triggers.count(_.isActive)

    if (hasPower != hadPower) {
      if (!hasPower) {
        active.foreach(_.onDisable(DisableReason.OutOfEnergy)) // This may change our energy buffer.
        hasPower = getLocalBuffer > 0 || Settings.get.ignorePower
      }
      else active.foreach(_.onEnable())
    }

    if (hasPower) {
      active.foreach(_.update())

      if (isServer) {
        if (player.getEntityWorld.getTotalWorldTime % Settings.get.tickFrequency == 0) {
          changeBuffer(-Settings.get.nanomachineCost * Settings.get.tickFrequency * (activeInputs + 0.5))
          PacketSender.sendNanomachinePower(player)
        }

        val overload = activeInputs - getSafeActiveInputs
        if (!player.capabilities.isCreativeMode && overload > 0 && player.getEntityWorld.getTotalWorldTime % 20 == 0) {
          player.attackEntityFrom(OverloadDamage, overload)
        }
      }

      if (isClient && Settings.get.enableNanomachinePfx) {
        val energyRatio = getLocalBuffer / (getLocalBufferSize + 1)
        val triggerRatio = activeInputs / (configuration.triggers.length + 1)
        val intensity = (energyRatio + triggerRatio) * 0.25
        PlayerUtils.spawnParticleAround(player, "portal", intensity)
      }
    }

    if (isServer) {
      // Send new power state, if it changed.
      if (hadPower != hasPower) {
        PacketSender.sendNanomachinePower(player)
      }

      // Send a full sync every now and then, e.g. for other players coming
      // closer that weren't there to get the initial info for an enabled
      // input.
      if (!hasSentConfiguration || player.getEntityWorld.getTotalWorldTime % FullSyncInterval == 0) {
        hasSentConfiguration = true
        PacketSender.sendNanomachineConfiguration(player)
      }
    }

    hadPower = hasPower
  }

  def reset(): Unit = {
    configuration.synchronized {
      for (index <- 0 until getTotalInputCount) {
        configuration.triggers(index).isActive = false
        activeBehaviorsDirty = true
      }
      cleanActiveBehaviors(DisableReason.Default)
    }
  }

  def dispose(): Unit = {
    reset()
    if (isServer) {
      api.Network.leaveWirelessNetwork(this)
    }
  }

  def debug(): Unit = {
    if (isServer) {
      configuration.debug()
      activeBehaviorsDirty = true
    }
  }

  def print(): Unit = {
    if (isServer) {
      configuration.print(player)
    }
  }

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

  def save(nbt: NBTTagCompound): Unit = configuration.synchronized {
    nbt.setString("uuid", uuid)
    nbt.setInteger("port", responsePort)
    nbt.setDouble("energy", storedEnergy)
    nbt.setNewCompoundTag("configuration", configuration.save)
  }

  def load(nbt: NBTTagCompound): Unit = configuration.synchronized {
    uuid = nbt.getString("uuid")
    responsePort = nbt.getInteger("port")
    storedEnergy = nbt.getDouble("energy")
    configuration.load(nbt.getCompoundTag("configuration"))
    activeBehaviorsDirty = true
  }

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

  private def isClient = world.isRemote

  private def isServer = !isClient

  private def cleanActiveBehaviors(reason: DisableReason): Unit = {
    if (activeBehaviorsDirty) {
      configuration.synchronized(if (activeBehaviorsDirty) {
        val newBehaviors = configuration.behaviors.filter(_.isActive).map(_.behavior)
        val addedBehaviors = newBehaviors -- activeBehaviors
        val removedBehaviors = activeBehaviors -- newBehaviors
        activeBehaviors.clear()
        activeBehaviors ++= newBehaviors
        activeBehaviorsDirty = false
        addedBehaviors.foreach(_.onEnable())
        removedBehaviors.foreach(_.onDisable(reason))

        if (isServer) {
          PacketSender.sendNanomachineInputs(player)
        }
      })
    }
  }
}
