package org.sidoh.reactor_simulator.simulator;

import java.util.HashSet;
import java.util.Set;

import cofh.api.energy.IEnergyHandler;
import cpw.mods.fml.common.network.simpleimpl.IMessage;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import erogenousbeef.bigreactors.api.data.CoilPartData;
import erogenousbeef.bigreactors.api.registry.TurbineCoil;
import erogenousbeef.bigreactors.common.BRLog;
import erogenousbeef.bigreactors.common.BigReactors;
import erogenousbeef.bigreactors.common.interfaces.IMultipleFluidHandler;
import erogenousbeef.bigreactors.common.multiblock.helpers.FloatUpdateTracker;
import erogenousbeef.bigreactors.common.multiblock.interfaces.IActivateable;
import erogenousbeef.bigreactors.common.multiblock.interfaces.ITickableMultiblockPart;
import erogenousbeef.bigreactors.common.multiblock.tileentity.TileEntityTurbinePartBase;
import erogenousbeef.bigreactors.common.multiblock.tileentity.TileEntityTurbinePartGlass;
import erogenousbeef.bigreactors.common.multiblock.tileentity.TileEntityTurbinePowerTap;
import erogenousbeef.bigreactors.common.multiblock.tileentity.TileEntityTurbineRotorBearing;
import erogenousbeef.bigreactors.common.multiblock.tileentity.TileEntityTurbineRotorPart;
import erogenousbeef.bigreactors.gui.container.ISlotlessUpdater;
import erogenousbeef.bigreactors.net.CommonPacketHandler;
import erogenousbeef.bigreactors.utils.StaticUtils;
import erogenousbeef.core.common.CoordTriplet;
import erogenousbeef.core.multiblock.IMultiblockPart;
import erogenousbeef.core.multiblock.MultiblockControllerBase;
import erogenousbeef.core.multiblock.MultiblockValidationException;
import erogenousbeef.core.multiblock.rectangular.RectangularMultiblockControllerBase;
import io.netty.buffer.ByteBuf;
import net.minecraft.block.Block;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import net.minecraftforge.fluids.Fluid;
import net.minecraftforge.fluids.FluidRegistry;
import net.minecraftforge.fluids.FluidStack;
import net.minecraftforge.fluids.FluidTank;
import net.minecraftforge.fluids.FluidTankInfo;

public class MultiblockTurbineSimulator extends RectangularMultiblockControllerBase implements IEnergyHandler, IMultipleFluidHandler, ISlotlessUpdater, IActivateable {

  public enum VentStatus {
    VentOverflow,
    VentAll,
    DoNotVent
  }

  public static final VentStatus[] s_VentStatuses = VentStatus.values();

  // UI updates
  private Set<EntityPlayer> updatePlayers;
  private int ticksSinceLastUpdate;
  private static final int ticksBetweenUpdates = 3;

  // Fluid tanks. Input = Steam, Output = Water.
  public static final int TANK_INPUT = 0;
  public static final int TANK_OUTPUT = 1;
  public static final int NUM_TANKS = 2;
  public static final int FLUID_NONE = -1;
  public static final int TANK_SIZE = 4000;
  public static final int MAX_PERMITTED_FLOW = 2000;

  private FluidTank[] tanks;

  static final float maxEnergyStored = 1000000f; // 1 MegaRF

  // Persistent game data
  float energyStored;
  boolean active;
  float rotorEnergy;
  boolean inductorEngaged;

  // Player settings
  VentStatus ventStatus;
  int maxIntakeRate;

  // Derivable game data
  int bladeSurfaceArea; // # of blocks that are blades
  int rotorMass; // 10 = 1 standard block-weight
  int coilSize;  // number of blocks in the coils

  // Inductor dynamic constants - get from a table on assembly
  float inductorDragCoefficient = inductorBaseDragCoefficient;
  float inductionEfficiency = 0.5f; // Final energy rectification efficiency. Averaged based on coil material and shape. 0.25-0.5 = iron, 0.75-0.9 = diamond, 1 = perfect.
  float inductionEnergyExponentBonus = 1f; // Exponential bonus to energy generation. Use this for very rare materials or special constructs.

  // Rotor dynamic constants - calculate on assembly
  float rotorDragCoefficient = 0.01f; // RF/t lost to friction per unit of mass in the rotor.
  float bladeDrag = 0.00025f; // RF/t lost to friction, multiplied by rotor speed squared.
  float frictionalDrag = 0f;

  // Penalize suboptimal shapes with worse drag (i.e. increased drag without increasing lift)
  // Suboptimal is defined as "not a christmas-tree shape". At worst, drag is increased 4x.

  // Game balance constants - some of these are modified by configs at startup
  public static int inputFluidPerBlade = 25; // mB
  public static float inductorBaseDragCoefficient = 0.1f; // RF/t extracted per coil block, multiplied by rotor speed squared.
  public static final float baseBladeDragCoefficient = 0.00025f; // RF/t base lost to aero drag per blade block. Includes a 50% reduction to factor in constant parts of the drag equation

  float energyGeneratedLastTick;
  int fluidConsumedLastTick;
  float rotorEfficiencyLastTick;

  private Set<IMultiblockPart> attachedControllers;
  private Set<TileEntityTurbineRotorBearing> attachedRotorBearings;

  private Set<TileEntityTurbinePowerTap> attachedPowerTaps;
  private Set<ITickableMultiblockPart> attachedTickables;

  private Set<TileEntityTurbineRotorPart> attachedRotorShafts;
  private Set<TileEntityTurbineRotorPart> attachedRotorBlades;

  private Set<TileEntityTurbinePartGlass> attachedGlass;

  // Data caches for validation
  private Set<CoordTriplet> foundCoils;

  private FloatUpdateTracker rpmUpdateTracker;

  private static final ForgeDirection[] RotorXBladeDirections = new ForgeDirection[]{ForgeDirection.UP, ForgeDirection.SOUTH, ForgeDirection.DOWN, ForgeDirection.NORTH};
  private static final ForgeDirection[] RotorZBladeDirections = new ForgeDirection[]{ForgeDirection.UP, ForgeDirection.EAST, ForgeDirection.DOWN, ForgeDirection.WEST};

  public MultiblockTurbineSimulator(World world) {
    super(world);

    updatePlayers = new HashSet<EntityPlayer>();

    ticksSinceLastUpdate = 0;

    tanks = new FluidTank[NUM_TANKS];
    for (int i = 0; i < NUM_TANKS; i++) {
      tanks[i] = new FluidTank(TANK_SIZE);
    }

    attachedControllers = new HashSet<IMultiblockPart>();
    attachedRotorBearings = new HashSet<TileEntityTurbineRotorBearing>();
    attachedPowerTaps = new HashSet<TileEntityTurbinePowerTap>();
    attachedTickables = new HashSet<ITickableMultiblockPart>();
    attachedRotorShafts = new HashSet<TileEntityTurbineRotorPart>();
    attachedRotorBlades = new HashSet<TileEntityTurbineRotorPart>();
    attachedGlass = new HashSet<TileEntityTurbinePartGlass>();

    energyStored = 0f;
    active = false;
    inductorEngaged = true;
    ventStatus = VentStatus.VentOverflow;
    rotorEnergy = 0f;
    maxIntakeRate = MAX_PERMITTED_FLOW;

    bladeSurfaceArea = 0;
    rotorMass = 0;
    coilSize = 0;
    energyGeneratedLastTick = 0f;
    fluidConsumedLastTick = 0;
    rotorEfficiencyLastTick = 1f;

    foundCoils = new HashSet<CoordTriplet>();

    rpmUpdateTracker = new FloatUpdateTracker(100, 5, 10f, 100f); // Minimum 10RPM difference for slow updates, if change > 100 RPM, update every 5 ticks
  }

  /**
   * Sends a full state update to a player.
   */
  protected void sendIndividualUpdate(EntityPlayer player) {
    if (this.worldObj.isRemote) {
      return;
    }

    CommonPacketHandler.INSTANCE.sendTo(getUpdatePacket(), (EntityPlayerMP)player);
  }

  protected IMessage getUpdatePacket() {
    return null;
  }

  /**
   * Send an update to any clients with GUIs open
   */
  protected void sendTickUpdate() {
    if (this.updatePlayers.size() <= 0) {
      return;
    }

    for (EntityPlayer player : updatePlayers) {
      CommonPacketHandler.INSTANCE.sendTo(getUpdatePacket(), (EntityPlayerMP)player);
    }
  }

  // MultiblockControllerBase overrides

  @Override
  public void onAttachedPartWithMultiblockData(IMultiblockPart part, NBTTagCompound data) {
    readFromNBT(data);
  }

  @Override
  protected void onBlockAdded(IMultiblockPart newPart) {
    if (newPart instanceof TileEntityTurbineRotorBearing) {
      this.attachedRotorBearings.add((TileEntityTurbineRotorBearing)newPart);
    }

    if (newPart instanceof TileEntityTurbinePowerTap) {
      attachedPowerTaps.add((TileEntityTurbinePowerTap)newPart);
    }

    if (newPart instanceof ITickableMultiblockPart) {
      attachedTickables.add((ITickableMultiblockPart)newPart);
    }

    if (newPart instanceof TileEntityTurbineRotorPart) {
      TileEntityTurbineRotorPart turbinePart = (TileEntityTurbineRotorPart)newPart;
      if (turbinePart.isRotorShaft()) {
        attachedRotorShafts.add(turbinePart);
      }

      if (turbinePart.isRotorBlade()) {
        attachedRotorBlades.add(turbinePart);
      }
    }

    if (newPart instanceof TileEntityTurbinePartGlass) {
      attachedGlass.add((TileEntityTurbinePartGlass)newPart);
    }

  }

  @Override
  protected void onBlockRemoved(IMultiblockPart oldPart) {
    if (oldPart instanceof TileEntityTurbineRotorBearing) {
      this.attachedRotorBearings.remove(oldPart);
    }

    if (oldPart instanceof TileEntityTurbinePowerTap) {
      attachedPowerTaps.remove((TileEntityTurbinePowerTap)oldPart);
    }

    if (oldPart instanceof ITickableMultiblockPart) {
      attachedTickables.remove((ITickableMultiblockPart)oldPart);
    }

    if (oldPart instanceof TileEntityTurbineRotorPart) {
      TileEntityTurbineRotorPart turbinePart = (TileEntityTurbineRotorPart)oldPart;
      if (turbinePart.isRotorShaft()) {
        attachedRotorShafts.remove(turbinePart);
      }

      if (turbinePart.isRotorBlade()) {
        attachedRotorBlades.remove(turbinePart);
      }
    }

    if (oldPart instanceof TileEntityTurbinePartGlass) {
      attachedGlass.remove((TileEntityTurbinePartGlass)oldPart);
    }
  }

  @Override
  protected void onMachineAssembled() {
  }

  @Override
  protected void onMachineRestored() {
  }

  @Override
  protected void onMachinePaused() {
  }

  @Override
  protected void onMachineDisassembled() {
    rotorMass = 0;
    bladeSurfaceArea = 0;
    coilSize = 0;

    rotorEnergy = 0f; // Kill energy when machines get broken by players/explosions
    rpmUpdateTracker.setValue(0f);
  }

  // Validation code
  @Override
  protected void isMachineWhole() throws MultiblockValidationException {
    if (attachedRotorBearings.size() != 1) {
      throw new MultiblockValidationException("Turbines require exactly 1 rotor bearing");
    }

    // Set up validation caches
    foundCoils.clear();

    super.isMachineWhole();

    // Now do additional validation based on the coils/blades/rotors that were found

    // Check that we have a rotor that goes all the way up the bearing
    TileEntityTurbinePartBase rotorPart = attachedRotorBearings.iterator().next();

    // Rotor bearing must calculate outwards dir, as this is normally only calculated in onMachineAssembled().
    rotorPart.recalculateOutwardsDirection(getMinimumCoord(), getMaximumCoord());

    // Find out which way the rotor runs. Obv, this is inwards from the bearing.
    ForgeDirection rotorDir = rotorPart.getOutwardsDir().getOpposite();
    CoordTriplet rotorCoord = rotorPart.getWorldLocation();

    CoordTriplet minRotorCoord = getMinimumCoord();
    CoordTriplet maxRotorCoord = getMaximumCoord();

    // Constrain min/max rotor coords to where the rotor bearing is and the block opposite it
    if (rotorDir.offsetX == 0) {
      minRotorCoord.x = maxRotorCoord.x = rotorCoord.x;
    }
    if (rotorDir.offsetY == 0) {
      minRotorCoord.y = maxRotorCoord.y = rotorCoord.y;
    }
    if (rotorDir.offsetZ == 0) {
      minRotorCoord.z = maxRotorCoord.z = rotorCoord.z;
    }

    // Figure out where the rotor ends and which directions are normal to the rotor's 4 faces (this is where blades emit from)
    CoordTriplet endRotorCoord = rotorCoord.equals(minRotorCoord) ? maxRotorCoord : minRotorCoord;
    endRotorCoord.translate(rotorDir.getOpposite());

    ForgeDirection[] bladeDirections;
    if (rotorDir.offsetY != 0) {
      bladeDirections = StaticUtils.CardinalDirections;
    } else if (rotorDir.offsetX != 0) {
      bladeDirections = RotorXBladeDirections;
    } else {
      bladeDirections = RotorZBladeDirections;
    }

    Set<CoordTriplet> rotorShafts = new HashSet<CoordTriplet>(attachedRotorShafts.size());
    Set<CoordTriplet> rotorBlades = new HashSet<CoordTriplet>(attachedRotorBlades.size());

    for (TileEntityTurbineRotorPart part : attachedRotorShafts) {
      rotorShafts.add(part.getWorldLocation());
    }

    for (TileEntityTurbineRotorPart part : attachedRotorBlades) {
      rotorBlades.add(part.getWorldLocation());
    }

    // Move along the length of the rotor, 1 block at a time
    boolean encounteredCoils = false;
    while (!rotorShafts.isEmpty() && !rotorCoord.equals(endRotorCoord)) {
      rotorCoord.translate(rotorDir);

      // Ensure we find a rotor block along the length of the entire rotor
      if (!rotorShafts.remove(rotorCoord)) {
        throw new MultiblockValidationException(String.format("%s - This block must contain a rotor. The rotor must begin at the bearing and run the entire length of the turbine", rotorCoord));
      }

      // Now move out in the 4 rotor normals, looking for blades and coils
      CoordTriplet checkCoord = rotorCoord.copy();
      boolean encounteredBlades = false;
      for (ForgeDirection bladeDir : bladeDirections) {
        checkCoord.copy(rotorCoord);
        boolean foundABlade = false;
        checkCoord.translate(bladeDir);

        // If we find 1 blade, we can keep moving along the normal to find more blades
        while (rotorBlades.remove(checkCoord)) {
          // We found a coil already?! NOT ALLOWED.
          if (encounteredCoils) {
            throw new MultiblockValidationException(String.format("%s - Rotor blades must be placed closer to the rotor bearing than all other parts inside a turbine", checkCoord));
          }
          foundABlade = encounteredBlades = true;
          checkCoord.translate(bladeDir);
        }

        // If this block wasn't a blade, check to see if it was a coil
        if (!foundABlade) {
          if (foundCoils.remove(checkCoord)) {
            encounteredCoils = true;

            // We cannot have blades and coils intermix. This prevents intermixing, depending on eval order.
            if (encounteredBlades) {
              throw new MultiblockValidationException(String.format("%s - Metal blocks must by placed further from the rotor bearing than all rotor blades", checkCoord));
            }

            // Check the two coil spots in the 'corners', which are permitted if they're connected to the main rotor coil somehow
            CoordTriplet coilCheck = checkCoord.copy();
            coilCheck.translate(bladeDir.getRotation(rotorDir));
            foundCoils.remove(coilCheck);
            coilCheck.copy(checkCoord);
            coilCheck.translate(bladeDir.getRotation(rotorDir.getOpposite()));
            foundCoils.remove(coilCheck);
          }
          // Else: It must have been air.
        }
      }
    }

    if (!rotorCoord.equals(endRotorCoord)) {
      throw new MultiblockValidationException("The rotor shaft must extend the entire length of the turbine interior.");
    }

    // Ensure that we encountered all the rotor, blade and coil blocks. If not, there's loose stuff inside the turbine.
    if (!rotorShafts.isEmpty()) {
      throw new MultiblockValidationException(String.format("Found %d rotor blocks that are not attached to the main rotor. All rotor blocks must be in a column extending the entire length of the turbine, starting from the bearing.", rotorShafts.size()));
    }

    if (!rotorBlades.isEmpty()) {
      throw new MultiblockValidationException(String.format("Found %d rotor blades that are not attached to the rotor. All rotor blades must extend continuously from the rotor's shaft.", rotorBlades.size()));
    }

    if (!foundCoils.isEmpty()) {
      throw new MultiblockValidationException(String.format("Found %d metal blocks which were not in a ring around the rotor. All metal blocks must be in rings, or partial rings, around the rotor.", foundCoils.size()));
    }

    // A-OK!
  }

  @Override
  protected void isBlockGoodForInterior(World world, int x, int y, int z) throws MultiblockValidationException {
    // We only allow air and functional parts in turbines.

    // Air is ok
    if (world.isAirBlock(x, y, z)) {
      return;
    }

    Block block = world.getBlock(x, y, z);
    int metadata = world.getBlockMetadata(x, y, z);

    // Coil windings below here:
    // Everything else, gtfo
    throw new MultiblockValidationException(String.format("%d, %d, %d is invalid for a turbine interior. Only rotor parts, metal blocks and empty space are allowed.", x, y, z));
  }

  @Override
  protected int getMinimumNumberOfBlocksForAssembledMachine() {
    // Hollow 5x5x4 cube (100 - 18), interior minimum is 3x3x2
    return 82;
  }

  @Override
  protected int getMaximumXSize() {
    return BigReactors.maximumTurbineSize;
  }

  @Override
  protected int getMaximumZSize() {
    return BigReactors.maximumTurbineSize;
  }

  @Override
  protected int getMaximumYSize() {
    return BigReactors.maximumTurbineHeight;
  }

  @Override
  protected int getMinimumXSize() {
    return 5;
  }

  @Override
  protected int getMinimumYSize() {
    return 4;
  }

  @Override
  protected int getMinimumZSize() {
    return 5;
  }


  @Override
  protected void onAssimilate(MultiblockControllerBase otherMachine) {
    if (!(otherMachine instanceof MultiblockTurbineSimulator)) {
      BRLog.warning("[%s] Turbine @ %s is attempting to assimilate a non-Turbine machine! That machine's data will be lost!", worldObj.isRemote ? "CLIENT" : "SERVER", getReferenceCoord());
      return;
    }

    MultiblockTurbineSimulator otherTurbine = (MultiblockTurbineSimulator)otherMachine;

    setRotorEnergy(Math.max(rotorEnergy, otherTurbine.rotorEnergy));
  }

  @Override
  protected void onAssimilated(MultiblockControllerBase assimilator) {
    attachedControllers.clear();
    attachedRotorBearings.clear();
    attachedTickables.clear();
    attachedPowerTaps.clear();
  }

  protected boolean updateServer() {
    return false;
  }

  protected static TurbineResult simulateServer(int steamIn, int levelsOfBlades, int bladesPerLevel, String inductorName, int inductorLevels, float rotorSpeed, int extraShaft) {
    float energyGeneratedLastTick = 0f;
    float fluidConsumedLastTick = 0;
    float rotorEfficiencyLastTick = 1f;
    int bladeSurfaceArea = levelsOfBlades * bladesPerLevel;
    int inductorBlockAmount = inductorLevels * 8;
    int rotorMass = bladeSurfaceArea * 10 + levelsOfBlades * 10 + inductorLevels * 10 + extraShaft *10;
    float rotorDragCoefficient = 0.01f;
    int rotorMomentum = bladeSurfaceArea * rotorMass;
    float rotorEnergy = rotorSpeed * rotorMomentum;

    float frictionalDrag = rotorMass * rotorDragCoefficient * BigReactors.turbineMassDragMultiplier;
    float bladeDrag = baseBladeDragCoefficient * bladeSurfaceArea * BigReactors.turbineAeroDragMultiplier;

    CoilPartData coilData = getCoilPartData(inductorName);
    float coilEfficiency = 0;
    float coilBonus = 0;
    float coilDragCoefficient = 0;
    int coilSize = 0;
    if (coilData != null) {
      for (int i = 0; i < inductorBlockAmount; i++) {
        coilEfficiency += coilData.efficiency;
        coilBonus += coilData.bonus;
        coilDragCoefficient += coilData.energyExtractionRate;
        coilSize += 1;
      }
    }

    float inductionEfficiency = 0f;
    float inductionEnergyExponentBonus = 1f;
    float inductorDragCoefficient = 0f;

    if (coilSize <= 0) {
      // Uh. No coil? Fine.
      inductionEfficiency = 0f;
      inductionEnergyExponentBonus = 1f;
      inductorDragCoefficient = 0f;
    } else {
      inductionEfficiency = (coilEfficiency * 0.33f) / coilSize;
      inductionEnergyExponentBonus = Math.max(1f, (coilBonus / coilSize));
      inductorDragCoefficient = (coilDragCoefficient / coilSize) * inductorBaseDragCoefficient;
    }

    // Generate energy based on steam
    if (steamIn > 0 || rotorEnergy > 0) {

      // RFs lost to aerodynamic drag.
      float aerodynamicDragTorque = (float)rotorSpeed * bladeDrag;

      float liftTorque = 0f;
      if (steamIn > 0) {
        // TODO: Lookup fluid parameters from a table
        float fluidEnergyDensity = 10f; // RF per mB

        // Cap amount of steam we can fully extract energy from based on blade size
        int steamToProcess = bladeSurfaceArea * inputFluidPerBlade;
        steamToProcess = Math.min(steamToProcess, steamIn);
        liftTorque = steamToProcess * fluidEnergyDensity;

        // Did we have excess steam for our blade size?
        if (steamToProcess < steamIn) {
          // Extract some percentage of the remaining steam's energy, based on how many blades are missing
          steamToProcess = steamIn - steamToProcess;
          float bladeEfficiency = 1f;
          int neededBlades = steamIn / inputFluidPerBlade; // round in the player's favor
          int missingBlades = neededBlades - bladeSurfaceArea;
          bladeEfficiency = 1f - (float)missingBlades / (float)neededBlades;
          liftTorque += steamToProcess * fluidEnergyDensity * bladeEfficiency;

          rotorEfficiencyLastTick = liftTorque / (steamIn * fluidEnergyDensity);
        }
      }

      // Yay for derivation. We're assuming delta-Time is always 1, as we're always calculating for 1 tick.
      // RFs available to coils
      float inductionTorque = rotorSpeed * inductorDragCoefficient * coilSize;
      float energyToGenerate = (float)Math.pow(inductionTorque, inductionEnergyExponentBonus) * inductionEfficiency;
      if (energyToGenerate > 0f) {
        // Efficiency curve. Rotors are 50% less efficient when not near 900/1800 RPMs.
        float efficiency = (float)(0.25 * Math.cos(rotorSpeed / (45.5 * Math.PI))) + 0.75f;
        if (rotorSpeed < 500) {
          efficiency = Math.min(0.5f, efficiency);
        }

        float newEnergy = energyToGenerate * efficiency;
        energyGeneratedLastTick += newEnergy * BigReactors.powerProductionMultiplier;
      }

      rotorEnergy += liftTorque + -1f * inductionTorque + -1f * aerodynamicDragTorque + -1f * frictionalDrag;
      if (rotorEnergy < 0f) {
        rotorEnergy = 0f;
      }

    }

    return new TurbineResult(energyGeneratedLastTick, rotorEfficiencyLastTick, (rotorEnergy / (rotorMomentum) - rotorSpeed));
  }

  public static class TurbineResult{
    public float energyGenerated;
    public float efficiency;
    public float changeInRotorSpeed;

    public TurbineResult(float energyGenerated, float efficiency, float changeInRotorSpeed) {
      this.energyGenerated = energyGenerated;
      this.efficiency = efficiency;
      this.changeInRotorSpeed = changeInRotorSpeed;
    }

    @Override
    public String toString() {
      return "TurbineResult{" +
          "energyGenerated=" + energyGenerated +
          ", efficiency=" + efficiency +
          ", changeInRotorSpeed=" + changeInRotorSpeed +
          '}';
    }
  }

  @Override
  protected void updateClient() {
  }

  @Override
  public void writeToNBT(NBTTagCompound data) {
    data.setTag("inputTank", tanks[TANK_INPUT].writeToNBT(new NBTTagCompound()));
    data.setTag("outputTank", tanks[TANK_OUTPUT].writeToNBT(new NBTTagCompound()));
    data.setBoolean("active", active);
    data.setFloat("energy", energyStored);
    data.setInteger("ventStatus", ventStatus.ordinal());
    data.setFloat("rotorEnergy", rotorEnergy);
    data.setInteger("maxIntakeRate", maxIntakeRate);
    data.setBoolean("inductorEngaged", inductorEngaged);
  }

  @Override
  public void readFromNBT(NBTTagCompound data) {
    if (data.hasKey("inputTank")) {
      tanks[TANK_INPUT].readFromNBT(data.getCompoundTag("inputTank"));
    }

    if (data.hasKey("outputTank")) {
      tanks[TANK_OUTPUT].readFromNBT(data.getCompoundTag("outputTank"));
    }

    if (data.hasKey("active")) {
      setActive(data.getBoolean("active"));
    }

    if (data.hasKey("energy")) {
      setEnergyStored(data.getFloat("energy"));
    }

    if (data.hasKey("ventStatus")) {
      setVentStatus(VentStatus.values()[data.getInteger("ventStatus")], false);
    }

    if (data.hasKey("rotorEnergy")) {
      setRotorEnergy(data.getFloat("rotorEnergy"));

      if (!worldObj.isRemote) {
        rpmUpdateTracker.setValue(getRotorSpeed());
      }
    }

    if (data.hasKey("maxIntakeRate")) {
      maxIntakeRate = data.getInteger("maxIntakeRate");
    }

    if (data.hasKey("inductorEngaged")) {
      setInductorEngaged(data.getBoolean("inductorEngaged"), false);
    }
  }

  @Override
  public void formatDescriptionPacket(NBTTagCompound data) {
    writeToNBT(data);
  }

  @Override
  public void decodeDescriptionPacket(NBTTagCompound data) {
    readFromNBT(data);
  }

  // Network Serialization

  /**
   * Used when dispatching update packets from the server.
   * @param buf ByteBuf into which the turbine's full status should be written
   */
  public void serialize(ByteBuf buf) {
    // Capture compacted fluid data first
    int inputFluidID, inputFluidAmt, outputFluidID, outputFluidAmt;
    {
      FluidStack inputFluid, outputFluid;
      inputFluid = tanks[TANK_INPUT].getFluid();
      outputFluid = tanks[TANK_OUTPUT].getFluid();

      if (inputFluid == null || inputFluid.amount <= 0) {
        inputFluidID = FLUID_NONE;
        inputFluidAmt = 0;
      } else {
        inputFluidID = inputFluid.getFluid().getID();
        inputFluidAmt = inputFluid.amount;
      }

      if (outputFluid == null || outputFluid.amount <= 0) {
        outputFluidID = FLUID_NONE;
        outputFluidAmt = 0;
      } else {
        outputFluidID = outputFluid.getFluid().getID();
        outputFluidAmt = outputFluid.amount;
      }
    }

    // User settings
    buf.writeBoolean(active);
    buf.writeBoolean(inductorEngaged);
    buf.writeInt(ventStatus.ordinal());
    buf.writeInt(maxIntakeRate);

    // Basic stats
    buf.writeFloat(energyStored);
    buf.writeFloat(rotorEnergy);

    // Reportage statistics
    buf.writeFloat(energyGeneratedLastTick);
    buf.writeInt(fluidConsumedLastTick);
    buf.writeFloat(rotorEfficiencyLastTick);

    // Fluid data
    buf.writeInt(inputFluidID);
    buf.writeInt(inputFluidAmt);
    buf.writeInt(outputFluidID);
    buf.writeInt(outputFluidAmt);
  }

  /**
   * Used when a status packet arrives on the client.
   * @param buf ByteBuf containing serialized turbine data
   */
  public void deserialize(ByteBuf buf) {
    // User settings
    setActive(buf.readBoolean());
    setInductorEngaged(buf.readBoolean(), false);
    setVentStatus(s_VentStatuses[buf.readInt()], false);
    setMaxIntakeRate(buf.readInt());

    // Basic data
    setEnergyStored(buf.readFloat());
    setRotorEnergy(buf.readFloat());

    // Reportage
    energyGeneratedLastTick = buf.readFloat();
    fluidConsumedLastTick = buf.readInt();
    rotorEfficiencyLastTick = buf.readFloat();

    // Fluid data
    int inputFluidID = buf.readInt();
    int inputFluidAmt = buf.readInt();
    int outputFluidID = buf.readInt();
    int outputFluidAmt = buf.readInt();

    if (inputFluidID == FLUID_NONE || inputFluidAmt <= 0) {
      tanks[TANK_INPUT].setFluid(null);
    } else {
      Fluid fluid = FluidRegistry.getFluid(inputFluidID);
      if (fluid == null) {
        BRLog.warning("[CLIENT] Multiblock Turbine received an unknown fluid of type %d, setting input tank to empty", inputFluidID);
        tanks[TANK_INPUT].setFluid(null);
      } else {
        tanks[TANK_INPUT].setFluid(new FluidStack(fluid, inputFluidAmt));
      }
    }

    if (outputFluidID == FLUID_NONE || outputFluidAmt <= 0) {
      tanks[TANK_OUTPUT].setFluid(null);
    } else {
      Fluid fluid = FluidRegistry.getFluid(outputFluidID);
      if (fluid == null) {
        BRLog.warning("[CLIENT] Multiblock Turbine received an unknown fluid of type %d, setting output tank to empty", outputFluidID);
        tanks[TANK_OUTPUT].setFluid(null);
      } else {
        tanks[TANK_OUTPUT].setFluid(new FluidStack(fluid, outputFluidAmt));
      }
    }
  }

  // Nondirectional FluidHandler implementation, similar to IFluidHandler
  public int fill(int tank, FluidStack resource, boolean doFill) {
    if (!canFill(tank, resource.getFluid())) {
      return 0;
    }

    return tanks[tank].fill(resource, doFill);
  }

  public FluidStack drain(int tank, FluidStack resource, boolean doDrain) {
    if (canDrain(tank, resource.getFluid())) {
      return tanks[tank].drain(resource.amount, doDrain);
    }

    return null;
  }

  public FluidStack drain(int tank, int maxDrain, boolean doDrain) {
    if (tank < 0 || tank >= NUM_TANKS) {
      return null;
    }

    return tanks[tank].drain(maxDrain, doDrain);
  }

  public boolean canFill(int tank, Fluid fluid) {
    if (tank < 0 || tank >= NUM_TANKS) {
      return false;
    }

    FluidStack fluidStack = tanks[tank].getFluid();
    if (fluidStack != null) {
      return fluidStack.getFluid().getID() == fluid.getID();
    } else if (tank == TANK_INPUT) {
      // TODO: Input tank can only be filled with compatible fluids from a registry
      return fluid.getName().equals("steam");
    } else {
      // Output tank can be filled with anything. Don't be a dumb.
      return true;
    }
  }

  public boolean canDrain(int tank, Fluid fluid) {
    if (tank < 0 || tank >= NUM_TANKS) {
      return false;
    }
    FluidStack fluidStack = tanks[tank].getFluid();
    if (fluidStack == null) {
      return false;
    }

    return fluidStack.getFluid().getID() == fluid.getID();
  }

  public FluidTankInfo[] getTankInfo() {
    FluidTankInfo[] infos = new FluidTankInfo[NUM_TANKS];
    for (int i = 0; i < NUM_TANKS; i++) {
      infos[i] = tanks[i].getInfo();
    }

    return infos;
  }

  public FluidTankInfo getTankInfo(int tankIdx) {
    return tanks[tankIdx].getInfo();
  }

  // IEnergyHandler

  @Override
  public int receiveEnergy(ForgeDirection from, int maxReceive, boolean simulate) {
    // haha no
    return 0;
  }

  @Override
  public int extractEnergy(ForgeDirection from, int maxExtract, boolean simulate) {
    int energyExtracted = Math.min((int)energyStored, maxExtract);

    if (!simulate) {
      energyStored -= energyExtracted;
    }

    return energyExtracted;
  }

  @Override
  public boolean canConnectEnergy(ForgeDirection from) {
    return true;
  }

  @Override
  public int getEnergyStored(ForgeDirection from) {
    return (int)energyStored;
  }

  @Override
  public int getMaxEnergyStored(ForgeDirection from) {
    return (int)maxEnergyStored;
  }

  private void setEnergyStored(float newEnergy) {
    if (Float.isInfinite(newEnergy) || Float.isNaN(newEnergy)) {
      return;
    }

    energyStored = Math.max(0f, Math.min(maxEnergyStored, newEnergy));
  }

  // Energy Helpers
  public float getEnergyStored() {
    return energyStored;
  }

  /**
   * Remove some energy from the internal storage buffer.
   * Will not reduce the buffer below 0.
   * @param energy Amount by which the buffer should be reduced.
   */
  protected void reduceStoredEnergy(float energy) {
    addStoredEnergy(-1f * energy);
  }

  /**
   * Add some energy to the internal storage buffer.
   * Will not increase the buffer above the maximum or reduce it below 0.
   * @param newEnergy
   */
  protected void addStoredEnergy(float newEnergy) {
    if (Float.isNaN(newEnergy)) {
      return;
    }

    energyStored += newEnergy;
    if (energyStored > maxEnergyStored) {
      energyStored = maxEnergyStored;
    }
    if (-0.00001f < energyStored && energyStored < 0.00001f) {
      // Clamp to zero
      energyStored = 0f;
    }
  }

  public void setStoredEnergy(float oldEnergy) {
    energyStored = oldEnergy;
    if (energyStored < 0.0 || Float.isNaN(energyStored)) {
      energyStored = 0.0f;
    } else if (energyStored > maxEnergyStored) {
      energyStored = maxEnergyStored;
    }
  }

  /**
   * Generate energy, internally. Will be multiplied by the BR Setting powerProductionMultiplier
   * @param newEnergy Base, unmultiplied energy to generate
   */
  protected void generateEnergy(float newEnergy) {
    energyGeneratedLastTick += newEnergy * BigReactors.powerProductionMultiplier;
    addStoredEnergy(newEnergy * BigReactors.powerProductionMultiplier);
  }

  // Activity state
  public boolean getActive() {
    return active;
  }

  public void setActive(boolean newValue) {
    if (newValue != active) {
      this.active = newValue;
      for (IMultiblockPart part : connectedParts) {
        if (this.active) {
          part.onMachineActivated();
        } else {
          part.onMachineDeactivated();
        }
      }

      CoordTriplet referenceCoord = getReferenceCoord();
      worldObj.markBlockForUpdate(referenceCoord.x, referenceCoord.y, referenceCoord.z);

      markReferenceCoordDirty();
    }

    if (worldObj.isRemote) {
      // Force controllers to re-render on client
      for (IMultiblockPart part : attachedControllers) {
        worldObj.markBlockForUpdate(part.xCoord, part.yCoord, part.zCoord);
      }

      for (TileEntityTurbineRotorPart part : attachedRotorBlades) {
        worldObj.markBlockForUpdate(part.xCoord, part.yCoord, part.zCoord);
      }

      for (TileEntityTurbineRotorPart part : attachedRotorShafts) {
        worldObj.markBlockForUpdate(part.xCoord, part.yCoord, part.zCoord);
      }
    }
  }

  // Governor
  public int getMaxIntakeRate() {
    return maxIntakeRate;
  }

  public void setMaxIntakeRate(int newRate) {
    maxIntakeRate = Math.min(MAX_PERMITTED_FLOW, Math.max(0, newRate));
    markReferenceCoordDirty();
  }

  // for GUI use
  public int getMaxIntakeRateMax() {
    return MAX_PERMITTED_FLOW;
  }

  // ISlotlessUpdater
  @Override
  public void beginUpdatingPlayer(EntityPlayer playerToUpdate) {
    updatePlayers.add(playerToUpdate);
    sendIndividualUpdate(playerToUpdate);
  }

  @Override
  public void stopUpdatingPlayer(EntityPlayer playerToRemove) {
    updatePlayers.remove(playerToRemove);
  }

  @Override
  public boolean isUseableByPlayer(EntityPlayer player) {
    return true;
  }

  private static CoilPartData getCoilPartData(String name) {
    return TurbineCoil.getBlockData(name);
  }

  /**
   * Recalculate rotor and coil parameters
   */
  //  private void recalculateDerivedStatistics() {
  //    CoordTriplet minInterior, maxInterior;
  //    minInterior = getMinimumCoord();
  //    maxInterior = getMaximumCoord();
  //    minInterior.x++;
  //    minInterior.y++;
  //    minInterior.z++;
  //    maxInterior.x++;
  //    maxInterior.y++;
  //    maxInterior.z++;
  //
  //    rotorMass = 0;
  //    bladeSurfaceArea = 0;
  //    coilSize = 0;
  //    float coilEfficiency = 0f;
  //    float coilBonus = 0f;
  //    float coilDragCoefficient = 0f;
  //
  //    // Loop over interior space. Calculate mass and blade area of rotor and size of coils
  //    for (int x = minInterior.x; x <= maxInterior.x; x++) {
  //      for (int y = minInterior.y; y <= maxInterior.y; y++) {
  //        for (int z = minInterior.z; z <= maxInterior.z; z++) {
  //          Block block = worldObj.getBlock(x, y, z);
  //          int metadata = worldObj.getBlockMetadata(x, y, z);
  //          CoilPartData coilData = null;
  //
  //          if (block == BigReactors.blockTurbineRotorPart) {
  //            rotorMass += BigReactors.blockTurbineRotorPart.getRotorMass(block, metadata);
  //            if (BlockTurbineRotorPart.isRotorBlade(metadata)) {
  //              bladeSurfaceArea += 1;
  //            }
  //          }
  //
  //          coilData = getCoilPartData(x, y, z, block, metadata);
  //          if (coilData != null) {
  //            coilEfficiency += coilData.efficiency;
  //            coilBonus += coilData.bonus;
  //            coilDragCoefficient += coilData.energyExtractionRate;
  //            coilSize += 1;
  //          }
  //        } // end z
  //      } // end y
  //    } // end x loop - looping over interior
  //
  //    // Precalculate some stuff now that we know how big the rotor and blades are
  //    frictionalDrag = rotorMass * rotorDragCoefficient * BigReactors.turbineMassDragMultiplier;
  //    bladeDrag = baseBladeDragCoefficient * bladeSurfaceArea * BigReactors.turbineAeroDragMultiplier;
  //
  //    if (coilSize <= 0) {
  //      // Uh. No coil? Fine.
  //      inductionEfficiency = 0f;
  //      inductionEnergyExponentBonus = 1f;
  //      inductorDragCoefficient = 0f;
  //    } else {
  //      inductionEfficiency = (coilEfficiency * 0.33f) / coilSize;
  //      inductionEnergyExponentBonus = Math.max(1f, (coilBonus / coilSize));
  //      inductorDragCoefficient = (coilDragCoefficient / coilSize) * inductorBaseDragCoefficient;
  //    }
  //  }
  public float getRotorSpeed() {
    if (attachedRotorBlades.size() <= 0 || rotorMass <= 0) {
      return 0f;
    }
    return rotorEnergy / (attachedRotorBlades.size() * rotorMass);
  }

  public float getEnergyGeneratedLastTick() {
    return energyGeneratedLastTick;
  }

  public int getFluidConsumedLastTick() {
    return fluidConsumedLastTick;
  }

  public int getNumRotorBlades() {
    return attachedRotorBlades.size();
  }

  public float getRotorEfficiencyLastTick() {
    return rotorEfficiencyLastTick;
  }

  public float getMaxRotorSpeed() {
    return 2000f;
  }

  public int getRotorMass() {
    return rotorMass;
  }

  public VentStatus getVentSetting() {
    return ventStatus;
  }

  public void setVentStatus(VentStatus newStatus, boolean markReferenceCoordDirty) {
    ventStatus = newStatus;
    if (markReferenceCoordDirty) {
      markReferenceCoordDirty();
    }
  }

  public boolean getInductorEngaged() {
    return inductorEngaged;
  }

  public void setInductorEngaged(boolean engaged, boolean markReferenceCoordDirty) {
    inductorEngaged = engaged;
    if (markReferenceCoordDirty) {
      markReferenceCoordDirty();
    }
  }


  private void setRotorEnergy(float newEnergy) {
    if (Float.isNaN(newEnergy) || Float.isInfinite(newEnergy)) {
      return;
    }
    rotorEnergy = Math.max(0f, newEnergy);
  }

  protected void markReferenceCoordDirty() {
    if (worldObj == null || worldObj.isRemote) {
      return;
    }

    CoordTriplet referenceCoord = getReferenceCoord();
    if (referenceCoord == null) {
      return;
    }

    rpmUpdateTracker.onExternalUpdate();

    TileEntity saveTe = worldObj.getTileEntity(referenceCoord.x, referenceCoord.y, referenceCoord.z);
    worldObj.markTileEntityChunkModified(referenceCoord.x, referenceCoord.y, referenceCoord.z, saveTe);
    worldObj.markBlockForUpdate(referenceCoord.x, referenceCoord.y, referenceCoord.z);
  }

  // For client usage only
  public ForgeDirection getRotorDirection() {
    if (attachedRotorBearings.size() < 1) {
      return ForgeDirection.UNKNOWN;
    }

    if (!this.isAssembled()) {
      return ForgeDirection.UNKNOWN;
    }

    TileEntityTurbineRotorBearing rotorBearing = attachedRotorBearings.iterator().next();
    return rotorBearing.getOutwardsDir().getOpposite();
  }

  public boolean hasGlass() {
    return attachedGlass.size() > 0;
  }

  public String getDebugInfo() {
    StringBuilder sb = new StringBuilder();
    sb.append("Assembled: ").append(Boolean.toString(isAssembled())).append("\n");
    sb.append("Attached Blocks: ").append(Integer.toString(connectedParts.size())).append("\n");
    if (getLastValidationException() != null) {
      sb.append("Validation Exception:\n").append(getLastValidationException().getMessage()).append("\n");
    }

    if (isAssembled()) {
      sb.append("\nActive: ").append(Boolean.toString(getActive()));
      sb.append("\nStored Energy: ").append(Float.toString(getEnergyStored()));
      sb.append("\nRotor Energy: ").append(Float.toString(rotorEnergy));
      sb.append("\nRotor Speed: ").append(Float.toString(getRotorSpeed())).append(" rpm");
      sb.append("\nInductor Engaged: ").append(Boolean.toString(inductorEngaged));
      sb.append("\nVent Status: ").append(ventStatus.toString());
      sb.append("\nMax Intake Rate: ").append(Integer.toString(maxIntakeRate));
      sb.append("\nCoil Size: ").append(Integer.toString(coilSize));
      sb.append("\nRotor Mass: ").append(Integer.toString(rotorMass));
      sb.append("\nBlade SurfArea: ").append(Integer.toString(bladeSurfaceArea));
      sb.append("\n# Blades: ").append(Integer.toString(attachedRotorBlades.size()));
      sb.append("\n# Shafts: ").append(Integer.toString(attachedRotorShafts.size()));
      sb.append("\nRotor Drag CoEff: ").append(Float.toString(rotorDragCoefficient));
      sb.append("\nBlade Drag: ").append(Float.toString(bladeDrag));
      sb.append("\nFrict Drag: ").append(Float.toString(frictionalDrag));
      sb.append("\n\nFluid Tanks:\n");
      for (int i = 0; i < tanks.length; i++) {
        sb.append(String.format("[%d] %s ", i, i == TANK_OUTPUT ? "outlet" : "inlet"));
        if (tanks[i] == null || tanks[i].getFluid() == null) {
          sb.append("empty");
        } else {
          FluidStack stack = tanks[i].getFluid();
          sb.append(String.format("%s, %d mB", stack.getFluid().getName(), stack.amount));
        }
        sb.append("\n");
      }
    }

    return sb.toString();
  }

  @SideOnly(Side.CLIENT)
  public void resetCachedRotors() {
    for (TileEntityTurbineRotorBearing bearing : attachedRotorBearings) {
      bearing.clearDisplayList();
    }
  }
}