import * as Calc from './Calculations';
import * as ModuleUtils from './ModuleUtils';
import * as Utils from '../utils/UtilityFunctions';
import { getBlueprint } from '../utils/BlueprintFunctions';
import Module from './Module';
import LZString from 'lz-string';
import * as _ from 'lodash';
import isEqual from 'lodash/lang';
import { Ships, Modifications } from 'coriolis-data/dist';
import { chain } from 'lodash';
const zlib = require('zlib');

const UNIQUE_MODULES = ['psg', 'sg', 'bsg', 'rf', 'fs', 'fh', 'gfsb', 'dc'];

// Constants for modifications struct
const SLOT_ID_DONE = -1;
const MODIFICATION_ID_DONE = -1;
const MODIFICATION_ID_BLUEPRINT = -2;
const MODIFICATION_ID_GRADE = -3;
const MODIFICATION_ID_SPECIAL = -4;

/**
 * Returns the power usage type of a slot and it's particular module
 * @param  {Object} slot      The Slot
 * @param  {Object} modul     The module in the slot
 * @return {String}           The key for the power usage type
 */
function powerUsageType(slot, modul) {
  if (modul) {
    if (modul.passive) {
      return 'retracted';
    }
  }
  return slot.cat != 1 ? 'retracted' : 'deployed';
}

/**
 * Populates the category array with module IDs from
 * the provided code
 * @param  {String} code    Serialized ship code
 * @param  {Array}  arr     Category array
 * @param  {Number} codePos Current position/Index of code string
 * @return {Number}         Next position/Index of code string
 */
function decodeToArray(code, arr, codePos) {
  for (let i = 0; i < arr.length; i++) {
    if (code.charAt(codePos) == '-') {
      arr[i] = 0;
      codePos++;
    } else {
      arr[i] = code.substring(codePos, codePos + 2);
      codePos += 2;
    }
  }
  return codePos;
}

/**
 * Reduce function used to get the IDs for a slot group (or array of slots)
 * @param  {array} idArray    The current Array of IDs
 * @param  {Object} slot      Slot object
 * @param  {integer} slotIndex The index for the slot in its group
 * @return {array}           The mutated idArray
 */
function reduceToIDs(idArray, slot, slotIndex) {
  idArray[slotIndex] = slot.m ? slot.m.id : '-';
  return idArray;
}

/**
 * Ship Model - Encapsulates and models in-game ship behavior
 */
export default class Ship {
  /**
   * @param {String} id         Unique ship Id / Key
   * @param {Object} properties Basic ship properties such as name, manufacturer, mass, etc
   * @param {Object} slots      Collection of slot groups (standard/standard, internal, hardpoints) with their max class size.
   */
  constructor(id, properties, slots) {
    this.id = id;
    this.serialized = {};
    this.cargoHatch = { m: ModuleUtils.cargoHatch(), type: 'SYS' };
    this.bulkheads = { incCost: true, maxClass: 8 };
    this.availCS = ModuleUtils.forShip(id);

    for (let p in properties) { this[p] = properties[p]; }  // Copy all base properties from shipData

    for (let slotType in slots) {   // Initialize all slots
      let slotGroup = slots[slotType];
      let group = this[slotType] = [];   // Initialize Slot group (Standard, Hardpoints, Internal)
      for (let slot of slotGroup) {
        if (typeof slot == 'object') {
          group.push({ m: null, incCost: true, maxClass: slot.class, eligible: slot.eligible });
        } else {
          group.push({ m: null, incCost: true, maxClass: slot });
        }
      }
    }
    // Make a Ship 'slot'/item similar to other slots
    this.m = { incCost: true, type: 'SHIP', discountedCost: this.hullCost, m: { class: '', rating: '', name: this.name, cost: this.hullCost } };
    this.costList = this.internal.concat(this.m, this.standard, this.hardpoints, this.bulkheads);
    this.powerList = this.internal.concat(
      this.cargoHatch,
      this.standard[0],  // Add Power Plant
      this.standard[2],  // Add FSD
      this.standard[1],  // Add Thrusters
      this.standard[4],  // Add Power Distributor
      this.standard[5],  // Add Sensors
      this.standard[3],  // Add Life Support
      this.hardpoints
    );
    this.moduleCostMultiplier = 1;
    this.priorityBands = [
      { deployed: 0, retracted: 0, },
      { deployed: 0, retracted: 0, },
      { deployed: 0, retracted: 0, },
      { deployed: 0, retracted: 0, },
      { deployed: 0, retracted: 0, }
    ];
  }

  /* GETTERS */

  /**
   * Can the ship thrust/move
   * @param  {Number} cargo     Amount of cargo in the ship
   * @param  {Number} fuel      Amount of fuel in the ship
   * @return {[type]} True if thrusters operational
   */
  canThrust(cargo, fuel) {
    return this.getSlotStatus(this.standard[1]) == 3 &&   // Thrusters are powered
        this.unladenMass + cargo + fuel < this.standard[1].m.getMaxMass(); // Max mass not exceeded
  }

  /**
   * Can the ship boost
   * @param  {Number} cargo     Amount of cargo in the ship
   * @param  {Number} fuel      Amount of fuel in the ship
   * @return {[type]} True if boost capable
   */
  canBoost(cargo, fuel) {
    return this.canThrust(cargo, fuel) &&                           // Thrusters operational
        this.standard[4].m.getEnginesCapacity() > this.boostEnergy; // PD capacitor is sufficient for boost
  }

  /**
   * Calculate the hypothetical laden jump range based on a potential change in mass, fuel, or FSD
   * @param  {Number} massDelta Optional - Change in laden mass (mass + cargo + fuel)
   * @param  {Number} fuel      Optional - Available fuel (defaults to max fuel based on FSD)
   * @param  {Object} fsd       Optional - Frame Shift Drive (or use mounted FSD)
   * @return {Number}           Jump range in Light Years
   */
  calcLadenRange(massDelta, fuel, fsd) {
    return Calc.jumpRange(this.ladenMass + (massDelta || 0), fsd || this.standard[2].m, fuel, this);
  }

  /**
   * Calculate the hypothetical unladen jump range based on a potential change in mass, fuel, or FSD
   * @param  {Number} massDelta Optional - Change in ship mass
   * @param  {Number} fuel      Optional - Available fuel (defaults to lesser of fuel capacity or max fuel based on FSD)
   * @param  {Object} fsd       Optional - Frame Shift Drive (or use mounted FSD)
   * @return {Number}           Jump range in Light Years
   */
  calcUnladenRange(massDelta, fuel, fsd) {
    fsd = fsd || this.standard[2].m;
    let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel;
    return Calc.jumpRange(this.unladenMass + (massDelta || 0) +  Math.min(fsdMaxFuelPerJump, fuel || this.fuelCapacity), fsd || this.standard[2].m, fuel, this);
  }

  /**
   * Calculate the hypothetical top speeds at cargo and fuel tonnage
   * @param  {Number} fuel  Fuel available in tons
   * @param  {Number} cargo Cargo in tons
   * @return {array}       Speed at pip settings
   */
  calcSpeedsWith(fuel, cargo) {
    return Calc.speed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed);
  }

  /**
   * Calculate the speed for a given configuration
   * @param  {Number}  eng   Number of pips in ENG
   * @param  {Number}  fuel  Amount of fuel carried
   * @param  {Number}  cargo Amount of cargo carried
   * @param  {boolean} boost true if boost is applied
   * @return {Number}        Speed
   */
  calcSpeed(eng, fuel, cargo, boost) {
    return Calc.calcSpeed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed, eng, this.boost / this.speed, boost);
  }

  /**
   * Calculate the pitch for a given configuration
   * @param  {Number}  eng   Number of pips in ENG
   * @param  {Number}  fuel  Amount of fuel carried
   * @param  {Number}  cargo Amount of cargo carried
   * @param  {boolean} boost true if boost is applied
   * @return {Number}        Pitch
   */
  calcPitch(eng, fuel, cargo, boost) {
    return Calc.calcPitch(this.unladenMass + fuel + cargo, this.pitch, this.standard[1].m, this.pipSpeed, eng, this.boost / this.speed, boost);
  }

  /**
   * Calculate the roll for a given configuration
   * @param  {Number}  eng   Number of pips in ENG
   * @param  {Number}  fuel  Amount of fuel carried
   * @param  {Number}  cargo Amount of cargo carried
   * @param  {boolean} boost true if boost is applied
   * @return {Number}        Roll
   */
  calcRoll(eng, fuel, cargo, boost) {
    return Calc.calcRoll(this.unladenMass + fuel + cargo, this.roll, this.standard[1].m, this.pipSpeed, eng, this.boost / this.speed, boost);
  }

  /**
   * Calculate the yaw for a given configuration
   * @param  {Number}  eng   Number of pips in ENG
   * @param  {Number}  fuel  Amount of fuel carried
   * @param  {Number}  cargo Amount of cargo carried
   * @param  {boolean} boost true if boost is applied
   * @return {Number}        Yaw
   */
  calcYaw(eng, fuel, cargo, boost) {
    return Calc.calcYaw(this.unladenMass + fuel + cargo, this.yaw, this.standard[1].m, this.pipSpeed, eng, this.boost / this.speed, boost);
  }

  /**
   * Calculate the hypothetical shield strength for the ship using the specified parameters
   * @param  {Object} sg              [optional] Shield Generator to use
   * @param  {Number} multiplierDelta [optional] Change to shield multiplier (+0.2, - 0.12, etc)
   * @return {Number}                 Shield strength in MJ
   */
  calcShieldStrengthWith(sg, multiplierDelta) {
    if (!sg) {
      let sgSlot = this.findInternalByGroup('sg');
      if (!sgSlot) {
        return 0;
      }
      sg = sgSlot.m;
    }
    // TODO Not accurate if the ship has modified shield boosters
    return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, 1 + (multiplierDelta || 0));
  }

  /**
   * Get the set of available modules for this ship
   * @return {ModuleSet} Available module set
   */
  getAvailableModules() {
    return this.availCS;
  }

  /**
   * Returns the a slots power status:
   *  0 - No status [Blank]
   *  1 - Disabled (Switched off)
   *  2 - Offline (Insufficient power available)
   *  3 - Online
   * @param  {Object} slot        Slot model
   * @param  {boolean} deployed   True - power used when hardpoints are deployed
   * @return {Number}             status index
   */
  getSlotStatus(slot, deployed) {
    if (!slot.m) { // Empty Slot
      return 0;   // No Status (Not possible to be active in this state)
    } else if (!slot.enabled) {
      return 1;   // Disabled
    } else if (deployed) {
      return this.priorityBands[slot.priority].deployedSum >= this.powerAvailable ? 2 : 3; // Offline : Online
      // Active hardpoints have no retracted status
    } else if ((slot.cat === 1 && !slot.m.passive)) {
      return 0;  // No Status (Not possible to be active in this state)
    }
    return this.priorityBands[slot.priority].retractedSum >= this.powerAvailable ? 2 : 3;    // Offline : Online
  }

  /**
   * Find an internal slot that has an installed modul of the specific group.
   *
   * @param  {String} group Module group/type
   * @return {Number}       The index of the slot in ship.internal
   */
  findInternalByGroup(group) {
    if (ModuleUtils.isShieldGenerator(group)) {
      return this.internal.find(slot => slot.m && ModuleUtils.isShieldGenerator(slot.m.grp));
    } else {
      return this.internal.find(slot => slot.m && slot.m.grp == group);
    }
  }

  /**
   * Find the shield generator for this ship
   * @return {object}       The shield generator module for this ship
   */
  findShieldGenerator() {
    const slot = this.internal.find(slot => slot.m && ModuleUtils.isShieldGenerator(slot.m.grp));
    return slot ? slot.m : undefined;
  }

  /**
   * Serializes the ship to a string
   * @return {String} Serialized ship 'code'
   */
  toString() {
    return [
      'A',
      this.getStandardString(),
      this.getHardpointsString(),
      this.getInternalString(),
      '.',
      this.getPowerEnabledString(),
      '.',
      this.getPowerPrioritiesString(),
      '.',
      this.getModificationsString()
    ].join('');
  }

  /**
   * Serializes the standard modules to a string
   * @return {String} Serialized standard modules 'code'
   */
  getStandardString() {
    if(!this.serialized.standard) {
      this.serialized.standard = this.bulkheads.m.index + this.standard.reduce((arr, slot, i) => {
        arr[i] = slot.m ? slot.m.id : '-';
        return arr;
      }, new Array(this.standard.length)).join('');
    }
    return this.serialized.standard;
  }

  /**
   * Serializes the internal modules to a string
   * @return {String} Serialized internal modules 'code'
   */
  getInternalString() {
    if(!this.serialized.internal) {
      this.serialized.internal = this.internal.reduce(reduceToIDs, new Array(this.internal.length)).join('');
    }
    return this.serialized.internal;
  }

  /**
   * Serializes the hardpoints and utility modules to a string
   * @return {String} Serialized hardpoints and utility modules 'code'
   */
  getHardpointsString() {
    if(!this.serialized.hardpoints) {
      this.serialized.hardpoints = this.hardpoints.reduce(reduceToIDs, new Array(this.hardpoints.length)).join('');
    }
    return this.serialized.hardpoints;
  }

  /**
   * Serializes the modifications to a string
   * @return {String} Serialized modifications 'code'
   */
  getModificationsString() {
    // Modifications can be updated outside of the ship's direct knowledge, for example when sliders change the value,
    // so always recreate it from scratch
    this.updateModificationsString();
    return this.serialized.modifications;
  }

  /**
   * Get the serialized module active/inactive settings
   * @return {String} Serialized active/inactive settings
   */
  getPowerEnabledString() {
    return this.serialized.enabled;
  }

  /**
   * Get the serialized module priority settings
   * @return {String} Serialized priority settings
   */
  getPowerPrioritiesString() {
    return this.serialized.priorities;
  }

  /* Mutate / Update Ship */

  /**
   * Recalculate all item costs and total based on discounts.
   * @param  {Number} shipDiscount      Ship cost discount (e.g. 0.1 === 10% discount)
   * @param  {Number} moduleDiscount    Module cost discount (e.g. 0.75 === 25% discount)
   * @return {this} The current ship instance for chaining
   */
  applyDiscounts(shipDiscount, moduleDiscount) {
    let shipCostMultiplier = 1 - shipDiscount;
    let moduleCostMultiplier = 1 - moduleDiscount;
    let total = 0;
    let costList = this.costList;

    for (let i = 0, l = costList.length; i < l; i++) {
      let item = costList[i];
      if (item.m && item.m.cost) {
        item.discountedCost = item.m.cost * (item.type == 'SHIP' ? shipCostMultiplier : moduleCostMultiplier);
        if (item.incCost) {
          total += item.discountedCost;
        }
      }
    }
    this.moduleCostMultiplier = moduleCostMultiplier;
    this.totalCost = total;
    return this;
  }

  /**
   * Clear all modification values for a module
   * @param  {Number} m      The module for which to clear the modifications
   */
  clearModifications(m) {
    m.mods = {};
    this.updatePowerGenerated()
      .updatePowerUsed()
      .recalculateMass()
      .updateJumpStats()
      .recalculateShield()
      .recalculateShieldCells()
      .recalculateArmour()
      .recalculateDps()
      .recalculateEps()
      .recalculateHps()
      .updateMovement();
  }

  /**
   * Set blueprint for a module
   * @param  {Object} m      The module for which to set the blueprint
   * @param  {Object} bp     The blueprint
   */
  setModuleBlueprint(m, bp) {
    m.blueprint = bp;
    this.clearModifications(m);
    // Set any hidden items for the blueprint now
    if (m.blueprint.grades[m.blueprint.grade] && m.blueprint.grades[m.blueprint.grade].features) {
      const features = m.blueprint.grades[m.blueprint.grade].features;
      for (const featureName in features) {
        if (Modifications.modifications[featureName].hidden) {
          this.setModification(m, featureName, bp.grades[bp.grade].features[featureName][0]);
        }
      }
    }

    this.updateModificationsString();
  }

  /**
   * Clear blueprint for a module
   * @param  {Object} m      The module for which to clear the blueprint
   */
  clearModuleBlueprint(m) {
    m.blueprint = {};
    this.updateModificationsString();
  }

  /**
   * Set special for a module
   * @param  {Object} m       The module for which to set the blueprint
   * @param  {Object} special The special
   */
  setModuleSpecial(m, special) {
    if (m.blueprint) {
      m.blueprint.special = special;
    }
    this.updatePowerGenerated()
      .updatePowerUsed()
      .recalculateMass()
      .updateJumpStats()
      .recalculateShield()
      .recalculateShieldCells()
      .recalculateArmour()
      .recalculateDps()
      .recalculateEps()
      .recalculateHps()
      .updateMovement();
  }

  /**
   * Clear special for a module
   * @param  {Object} m      The module for which to clear the blueprint
   */
  clearModuleSpecial(m) {
    this.setModuleSpecial(m, null);
  }

  /**
   * Set a modification value and update ship stats
   * @param {Object} m          The module to change
   * @param {Object} name       The name of the modification to change
   * @param {Number} value The new value of the modification.  The value of the modification is scaled to provide two decimal places of precision in an integer.  For example 1.23% is stored as 123
   * @param {boolean}   isAbsolute True if value is an absolute value and not a modification value
   */
  setModification(m, name, value, isAbsolute = false) {
    if (isNaN(value)) {
      // Value passed is invalid; reset it to 0
      value = 0;
    }

    if (isAbsolute) {
      m.setPretty(name, value, isAbsolute);
    } else {
      m.setModValue(name, value, false);
    }

    // Handle special cases
    if (name === 'pgen') {
      // Power generation
      this.updatePowerGenerated();
    } else if (name === 'power') {
      // Power usage
      this.updatePowerUsed();
    } else if (name === 'mass') {
      // Mass
      this.recalculateMass();
      this.updateMovement();
      this.updateJumpStats();
    } else if (name === 'maxfuel') {
      this.updateJumpStats();
    } else if (name === 'optmass') {
      // Could be for any of thrusters, FSD or shield
      this.updateMovement();
      this.updateJumpStats();
      this.recalculateShield();
    } else if (name === 'optmul') {
      // Could be for any of thrusters, FSD or shield
      this.updateMovement();
      this.updateJumpStats();
      this.recalculateShield();
    } else if (name === 'shieldboost') {
      this.recalculateShield();
    } else if (name === 'hullboost' || name === 'hullreinforcement' || name === 'modulereinforcement') {
      this.recalculateArmour();
    } else if (name === 'shieldreinforcement') {
      this.recalculateShieldCells();
    } else if (name === 'burst' || name === 'burstrof' || name === 'clip' || name === 'damage' || name === 'distdraw' || name === 'jitter' || name === 'piercing' || name === 'range' || name === 'reload' || name === 'rof' || name === 'thermload') {
      this.recalculateDps();
      this.recalculateHps();
      this.recalculateEps();
    } else if (name === 'explres' || name === 'kinres' || name === 'thermres') {
      // Could be for shields or armour
      this.recalculateArmour();
      this.recalculateShield();
    } else if (name === 'engcap') {
      // Might have resulted in a change in boostability
      this.updateMovement();
    }
  }

  /**
   * Builds/Updates the ship instance with the ModuleUtils[comps] passed in.
   * @param {Object} comps Collection of ModuleUtils used to build the ship
   * @param {array} priorities Slot priorities
   * @param {Array} enabled    Slot active/inactive
   * @param {Array} mods       Modifications
   * @param {Array} blueprints Blueprints for modifications
   * @return {this} The current ship instance for chaining
   */
  buildWith(comps, priorities, enabled, mods, blueprints) {
    let internal = this.internal,
        standard = this.standard,
        hps = this.hardpoints,
        cl = standard.length,
        i, l;

    // Reset Cumulative stats
    this.fuelCapacity = 0;
    this.cargoCapacity = 0;
    this.passengerCapacity = 0;
    this.ladenMass = 0;
    this.armour = this.baseArmour;
    this.shield = this.baseShieldStrength;
    this.shieldCells = 0;
    this.totalCost = this.m.incCost ? this.m.discountedCost : 0;
    this.unladenMass = this.hullMass;
    this.totalDpe = 0;
    this.totalAbsDpe = 0;
    this.totalExplDpe = 0;
    this.totalKinDpe = 0;
    this.totalThermDpe = 0;
    this.totalDps = 0;
    this.totalAbsDps = 0;
    this.totalExplDps = 0;
    this.totalKinDps = 0;
    this.totalThermDps = 0;
    this.totalSDps = 0;
    this.totalAbsSDps = 0;
    this.totalExplSDps = 0;
    this.totalKinSDps = 0;
    this.totalThermSDps = 0;
    this.totalEps = 0;
    this.totalHps = 0;
    this.shieldExplRes = 0;
    this.shieldKinRes = 0;
    this.shieldThermRes = 0;
    this.hullExplRes = 0;
    this.hullKinRes = 0;
    this.hullThermRes = 0;

    this.bulkheads.m = null;
    this.useBulkhead(comps && comps.bulkheads ? comps.bulkheads : 0, true);
    this.bulkheads.m.mods = mods && mods[0] ? mods[0] : {};
    if (blueprints && blueprints[0]) {
      this.bulkheads.m.blueprint = getBlueprint(blueprints[0].fdname, this.bulkheads.m);
      this.bulkheads.m.blueprint.grade = blueprints[0].grade;
      this.bulkheads.m.blueprint.special = blueprints[0].special;
    } else {
      this.bulkheads.m.blueprint = {};
    }
    this.cargoHatch.priority = priorities ? priorities[0] * 1 : 0;
    this.cargoHatch.enabled = enabled ? enabled[0] * 1 : true;

    for (i = 0; i < cl; i++) {
      standard[i].cat = 0;
      standard[i].priority = priorities && priorities[i + 1] ? priorities[i + 1] * 1 : 0;
      standard[i].type = 'SYS';
      standard[i].m = null; // Resetting 'old' module if there was one
      standard[i].discountedCost = 0;
      if (comps) {
        let module = ModuleUtils.standard(i, comps.standard[i]);
        if (module != null) {
          module.mods = mods && mods[i + 1] ? mods[i + 1] : {};
          if (blueprints && blueprints[i + 1]) {
            module.blueprint = getBlueprint(blueprints[i + 1].fdname, module);
            module.blueprint.grade = blueprints[i + 1].grade;
            module.blueprint.special = blueprints[i + 1].special;
          } else {
            module.blueprint = {};
          }
        }
        this.use(standard[i], module, true);
      }
      standard[i].enabled = enabled ? enabled[i + 1] * 1 : true;
    }

    standard[1].type = 'ENG'; // Thrusters
    standard[2].type = 'ENG'; // FSD
    cl++; // Increase accounts for Cargo Scoop

    for (i = 0, l = hps.length; i < l; i++) {
      hps[i].cat = 1;
      hps[i].priority = priorities && priorities[cl + i] ? priorities[cl + i] * 1 : 0;
      hps[i].type = hps[i].maxClass ? 'WEP' : 'SYS';
      hps[i].m = null; // Resetting 'old' modul if there was one
      hps[i].discountedCost = 0;

      if (comps && comps.hardpoints[i] !== 0) {
        let module = ModuleUtils.hardpoints(comps.hardpoints[i]);
        if (module != null) {
          module.mods = mods && mods[cl + i] ? mods[cl + i] : {};
          if (blueprints && blueprints[cl + i]) {
            module.blueprint = getBlueprint(blueprints[cl + i].fdname, module);
            module.blueprint.grade = blueprints[cl + i].grade;
            module.blueprint.special = blueprints[cl + i].special;
          } else {
            module.blueprint = {};
          }
        }
        this.use(hps[i], module, true);
      }
      hps[i].enabled = enabled ? enabled[cl + i] * 1 : true;
    }

    cl += hps.length; // Increase accounts for hardpoints

    for (i = 0, l = internal.length; i < l; i++) {
      internal[i].cat = 2;
      internal[i].priority = priorities && priorities[cl + i] ? priorities[cl + i] * 1 : 0;
      internal[i].type = 'SYS';
      internal[i].m = null; // Resetting 'old' modul if there was one
      internal[i].discountedCost = 0;

      if (comps && comps.internal[i] !== 0) {
        let module = ModuleUtils.internal(comps.internal[i]);
        if (module != null) {
          module.mods = mods && mods[cl + i] ? mods[cl + i] : {};
          if (blueprints && blueprints[cl + i]) {
            module.blueprint = getBlueprint(blueprints[cl + i].fdname, module);
            module.blueprint.grade = blueprints[cl + i].grade;
            module.blueprint.special = blueprints[cl + i].special;
          } else {
            module.blueprint = {};
          }
        }
        this.use(internal[i], module, true);
      }
      internal[i].enabled = enabled ? enabled[cl + i] * 1 : true;
    }

    // Update aggragated stats
    if (comps) {
      this.updatePowerGenerated()
        .updatePowerUsed()
        .recalculateMass()
        .updateJumpStats()
        .recalculateShield()
        .recalculateShieldCells()
        .recalculateArmour()
        .recalculateDps()
        .recalculateEps()
        .recalculateHps()
        .updateMovement();
    }

    return this.updatePowerPrioritesString().updatePowerEnabledString().updateModificationsString();
  }

  /**
   * Updates an existing ship instance's slots with modules determined by the
   * code.
   *
   * @param {String}  serializedString  The string to deserialize
   * @return {this} The current ship instance for chaining
   */
  buildFrom(serializedString) {
    if (!serializedString) {
      // Empty serialized string; nothing to do
      return this;
    }

    let standard = new Array(this.standard.length),
        hardpoints = new Array(this.hardpoints.length),
        internal = new Array(this.internal.length),
        modifications = new Array(1 + this.standard.length + this.hardpoints.length + this.internal.length),
        blueprints = new Array(1 + this.standard.length + this.hardpoints.length + this.internal.length),
        parts = serializedString.split('.'),
        priorities = null,
        enabled = null,
        code = parts[0];

    // Code has a version ID embedded as the first character (if it is alphabetic)
    let version;
    if (code && code.match(/^[0-4]/)) {
      // Starting with bulkhead number is version 1
      version = 1;
    } else {
      // Version 2 (current version)
      version = 2;
      if (code) {
        code = code.substring(1);
      }
    }

    if (parts[1]) {
      enabled = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[1])).split('');
    }

    if (parts[2]) {
      priorities = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[2])).split('');
    }

    if (parts[3]) {
      const modstr = parts[3];
      if (modstr.match(':')) {
        this.decodeModificationsString(modstr, modifications);
      } else {
        try {
          this.decodeModificationsStruct(zlib.gunzipSync(new Buffer(Utils.fromUrlSafe(modstr), 'base64')), modifications, blueprints);
        } catch (err) {
          // Could be out-of-date URL; ignore
        }
      }
    }

    decodeToArray(code, internal, decodeToArray(code, hardpoints, decodeToArray(code, standard, 1)));

    if (version != 2) {
      // Alter as required due to changes in the (build) code from one version to the next
      this.upgradeInternals(internal, 1 + this.standard.length + this.hardpoints.length, priorities, enabled, modifications, blueprints, version);
    }

    return this.buildWith(
      {
        bulkheads: code.charAt(0) * 1,
        standard,
        hardpoints,
        internal
      },
      priorities,
      enabled,
      modifications,
      blueprints,
    );
  };

  /**
   * Empties all hardpoints and utility slots
   * @return {this} The current ship instance for chaining
   */
  emptyHardpoints() {
    for (let i = this.hardpoints.length; i--;) {
      this.use(this.hardpoints[i], null);
    }
    return this;
  }

  /**
   * Empties all Internal slots
   * @return {this} The current ship instance for chaining
   */
  emptyInternal() {
    for (let i = this.internal.length; i--;) {
      this.use(this.internal[i], null);
    }
    return this;
  }

  /**
   * Empties all Utility slots
   * @return {this} The current ship instance for chaining
   */
  emptyUtility() {
    for (let i = this.hardpoints.length; i--;) {
      if (!this.hardpoints[i].maxClass) {
        this.use(this.hardpoints[i], null);
      }
    }
    return this;
  }

  /**
   * Empties all hardpoints
   * @return {this} The current ship instance for chaining
   */
  emptyWeapons() {
    for (let i = this.hardpoints.length; i--;) {
      if (this.hardpoints[i].maxClass) {
        this.use(this.hardpoints[i], null);
      }
    }
    return this;
  }

  /**
   * Optimize for the lower mass build that can still boost and power the ship
   * without power management.
   * @param  {Object} m Standard Module overrides
   * @return {this} The current ship instance for chaining
   */
  optimizeMass(m) {
    return this.emptyHardpoints().emptyInternal().useLightestStandard(m);
  }

  /**
   * Include/Exclude a item/slot in cost calculations
   * @param {Object} item       Slot or item
   * @param {boolean} included  Cost included
   * @return {this} The current ship instance for chaining
   */
  setCostIncluded(item, included) {
    if (item.incCost != included && item.m) {
      this.totalCost += included ? item.discountedCost : -item.discountedCost;
    }
    item.incCost = included;
    return this;
  }

  /**
   * Set slot active/inactive
   * @param {Object} slot    Slot model
   * @param {boolean} enabled  True - active
   * @return {this} The current ship instance for chaining
   */
  setSlotEnabled(slot, enabled) {
    if (slot.enabled != enabled) { // Enabled state is changing
      slot.enabled = enabled;
      if (slot.m) {
        if (ModuleUtils.isShieldGenerator(slot.m.grp) || slot.m.grp === 'sb') {
          this.recalculateShield();
        }
        if (slot.m.grp === 'scb') {
          this.recalculateShieldCells();
        }

        this.updatePowerUsed();
        this.updatePowerEnabledString();

        if (slot.m.getDps()) {
          this.recalculateDps();
        }

        if (slot.m.getHps()) {
          this.recalculateHps();
        }

        if (slot.m.getEps()) {
          this.recalculateEps();
        }
      }
    }
    return this;
  }

  /**
   * Will change the priority of the specified slot if the new priority is valid
   * @param  {Object} slot        The slot to be updated
   * @param  {Number} newPriority The new priority to be set
   * @return {boolean}            Returns true if the priority was changed (within range)
   */
  setSlotPriority(slot, newPriority) {
    if (newPriority >= 0 && newPriority < this.priorityBands.length) {
      slot.priority = newPriority;
      this.updatePowerPrioritesString();

      if (slot.enabled) { // Only update power if the slot is enabled
        this.updatePowerUsed();
      }
      return true;
    }
    return false;
  }

  /**
   * Updates the ship's cumulative and aggregated stats based on the module change.
   * @param  {Object} slot            The slot being updated
   * @param  {Object} n               The new module (may be null)
   * @param  {Object} old             The old module (may be null)
   * @param  {boolean} preventUpdate  If true the global ship state will not be updated
   * @return {this}                   The ship instance (for chaining operations)
   */
  updateStats(slot, n, old, preventUpdate) {
    let powerGeneratedChange = slot == this.standard[0];
    let powerDistributorChange = slot == this.standard[4];
    let powerUsedChange = false;
    let dpsChanged = n && n.getDps() || old && old.getDps();
    let epsChanged = n && n.getEps() || old && old.getEps();
    let hpsChanged = n && n.getHps() || old && old.getHps();

    let armourChange = (slot === this.bulkheads) ||
      (n && n.grp === 'hr') || (old && old.grp === 'hr') ||
      (n && n.grp === 'ghrp') || (old && old.grp === 'ghrp') ||
      (n && n.grp == 'mahr') || (old && old.grp == 'mahr') ||
      (n && n.grp === 'mrp') || (old && old.grp === 'mrp') ||
      (n && n.grp === 'gmrp') || (old && old.grp == 'gmrp');

    let shieldChange = (n && n.grp === 'bsg') || (old && old.grp === 'bsg') || (n && n.grp === 'psg') || (old && old.grp === 'psg') || (n && n.grp === 'sg') || (old && old.grp === 'sg') || (n && n.grp === 'sb') || (old && old.grp === 'sb') || (old && old.grp === 'gsrp') || (n && n.grp === 'gsrp');

    let shieldCellsChange = (n && n.grp === 'scb') || (old && old.grp === 'scb');

    if (old) {  // Old modul now being removed
      if (slot.incCost && old.cost) {
        this.totalCost -= old.cost * this.moduleCostMultiplier;
      }

      if (old.getPowerUsage() > 0 && slot.enabled) {
        powerUsedChange = true;
      }
    }

    if (n) {
      if (slot.incCost && n.cost) {
        this.totalCost += n.cost * this.moduleCostMultiplier;
      }

      if (n.power && slot.enabled) {
        powerUsedChange = true;
      }
    }

    if (!preventUpdate) {
      // Must recalculate mass first, as movement, jump etc. relies on it
      this.recalculateMass();
      if (dpsChanged) {
        this.recalculateDps();
      }
      if (epsChanged) {
        this.recalculateEps();
      }
      if (hpsChanged) {
        this.recalculateHps();
      }
      if (powerGeneratedChange) {
        this.updatePowerGenerated();
      }
      if (powerUsedChange) {
        this.updatePowerUsed();
      }
      if (armourChange) {
        this.recalculateArmour();
      }
      if (shieldChange) {
        this.recalculateShield();
      }
      if (shieldCellsChange) {
        this.recalculateShieldCells();
      }
      this.updateMovement();
      this.updateJumpStats();
    }
    return this;
  }

  /**
   * Calculate damage per second and related items for weapons
   * @return {this} The ship instance (for chaining operations)
   */
  recalculateDps() {
    let totalDpe = 0;
    let totalAbsDpe = 0;
    let totalExplDpe = 0;
    let totalKinDpe = 0;
    let totalThermDpe = 0;
    let totalDps = 0;
    let totalAbsDps = 0;
    let totalExplDps = 0;
    let totalKinDps = 0;
    let totalThermDps = 0;
    let totalSDps = 0;
    let totalAbsSDps = 0;
    let totalExplSDps = 0;
    let totalKinSDps = 0;
    let totalThermSDps = 0;

    for (let slotNum in this.hardpoints) {
      const slot = this.hardpoints[slotNum];
      if (slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getDps()) {
        const dpe = slot.m.getEps() === 0 ? 0 : slot.m.getDps() / slot.m.getEps();
        const dps = slot.m.getDps();
        const sdps = slot.m.getClip() ? (slot.m.getClip() * slot.m.getDps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : dps;

        totalDpe += dpe;
        totalDps += dps;
        totalSDps += sdps;
        if (slot.m.getDamageDist()) {
          if (slot.m.getDamageDist().A) {
            totalAbsDpe += dpe * slot.m.getDamageDist().A;
            totalAbsDps += dps * slot.m.getDamageDist().A;
            totalAbsSDps += sdps * slot.m.getDamageDist().A;
          }
          if (slot.m.getDamageDist().E) {
            totalExplDpe += dpe * slot.m.getDamageDist().E;
            totalExplDps += dps * slot.m.getDamageDist().E;
            totalExplSDps += sdps * slot.m.getDamageDist().E;
          }
          if (slot.m.getDamageDist().K) {
            totalKinDpe += dpe * slot.m.getDamageDist().K;
            totalKinDps += dps * slot.m.getDamageDist().K;
            totalKinSDps += sdps * slot.m.getDamageDist().K;
          }
          if (slot.m.getDamageDist().T) {
            totalThermDpe += dpe * slot.m.getDamageDist().T;
            totalThermDps += dps * slot.m.getDamageDist().T;
            totalThermSDps += sdps * slot.m.getDamageDist().T;
          }
        }
      }
    }

    this.totalDpe = totalDpe;
    this.totalAbsDpe = totalAbsDpe;
    this.totalExplDpe = totalExplDpe;
    this.totalKinDpe = totalKinDpe;
    this.totalThermDpe = totalThermDpe;
    this.totalDps = totalDps;
    this.totalAbsDps = totalAbsDps;
    this.totalExplDps = totalExplDps;
    this.totalKinDps = totalKinDps;
    this.totalThermDps = totalThermDps;
    this.totalSDps = totalSDps;
    this.totalAbsSDps = totalAbsSDps;
    this.totalExplSDps = totalExplSDps;
    this.totalKinSDps = totalKinSDps;
    this.totalThermSDps = totalThermSDps;

    return this;
  }

  /**
   * Calculate heat per second for weapons
   * @return {this} The ship instance (for chaining operations)
   */
  recalculateHps() {
    let totalHps = 0;

    for (let slotNum in this.hardpoints) {
      const slot = this.hardpoints[slotNum];
      if (slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getHps()) {
        totalHps += slot.m.getHps();
      }
    }
    this.totalHps = totalHps;

    return this;
  }

  /**
   * Calculate energy per second for weapons
   * @return {this} The ship instance (for chaining operations)
   */
  recalculateEps() {
    let totalEps = 0;

    for (let slotNum in this.hardpoints) {
      const slot = this.hardpoints[slotNum];
      if (slot.m && slot.enabled && slot.m.getEps() && slot.type === 'WEP') {
        totalEps += slot.m.getEps();
      }
    }
    this.totalEps = totalEps;

    return this;
  }

  /**
   * Update power calculations when amount generated changes
   * @return {this} The ship instance (for chaining operations)
   */
  updatePowerGenerated() {
    this.powerAvailable = this.standard[0].m.getPowerGeneration();
    return this;
  };

  /**
   * Update power calculations when amount consumed changes
   * @return {this} The ship instance (for chaining operations)
   */
  updatePowerUsed() {
    let bands = [
      { deployed: 0, retracted: 0, },
      { deployed: 0, retracted: 0, },
      { deployed: 0, retracted: 0, },
      { deployed: 0, retracted: 0, },
      { deployed: 0, retracted: 0, }
    ];

    if (this.cargoHatch.enabled) {
      bands[this.cargoHatch.priority].retracted += this.cargoHatch.m.getPowerUsage();
    }

    for (let slotNum in this.standard) {
      const slot = this.standard[slotNum];
      if (slot.m && slot.enabled) {
        bands[slot.priority][powerUsageType(slot, slot.m)] += slot.m.getPowerUsage();
      }
    }

    for (let slotNum in this.internal) {
      const slot = this.internal[slotNum];
      if (slot.m && slot.enabled) {
        bands[slot.priority][powerUsageType(slot, slot.m)] += slot.m.getPowerUsage();
      }
    }

    for (let slotNum in this.hardpoints) {
      const slot = this.hardpoints[slotNum];
      if (slot.m && slot.enabled) {
        bands[slot.priority][powerUsageType(slot, slot.m)] += slot.m.getPowerUsage();
      }
    }

    // Work out the running totals
    let prevRetracted = 0, prevDeployed = 0;
    for (let i = 0, l = bands.length; i < l; i++) {
      let band = bands[i];
      prevRetracted = band.retractedSum = prevRetracted + band.retracted;
      prevDeployed = band.deployedSum = prevDeployed + band.deployed + band.retracted;
    }

    // Update global stats
    this.powerRetracted = prevRetracted;
    this.powerDeployed = prevDeployed;
    this.priorityBands = bands;

    return this;
  }

  /**
   * Eecalculate mass
   * @return {this} The ship instance (for chaining operations)
   */
  recalculateMass() {
    let unladenMass = this.hullMass;
    let cargoCapacity = 0;
    let fuelCapacity = 0;
    let passengerCapacity = 0;

    unladenMass += this.bulkheads.m.getMass();

    let slots = this.standard.concat(this.internal, this.hardpoints);
    // TODO: create class for slot and also add slot.get
    // handle unladen mass
    unladenMass += chain(slots)
      .map(slot => slot.m ? slot.m.get('mass') : null)
      .map(mass => mass || 0)
      .reduce((sum, mass) => sum + mass)
      .value();

    // handle fuel capacity
    fuelCapacity += chain(slots)
      .map(slot => slot.m ? slot.m.get('fuel') : null)
      .map(fuel => fuel || 0)
      .reduce((sum, fuel) => sum + fuel)
      .value();

    // handle cargo capacity
    cargoCapacity += chain(slots)
      .map(slot => slot.m ? slot.m.get('cargo') : null)
      .map(cargo => cargo || 0)
      .reduce((sum, cargo) => sum + cargo)
      .value();

    // handle passenger capacity
    passengerCapacity += chain(slots)
      .map(slot => slot.m ? slot.m.get('passengers') : null)
      .map(passengers => passengers || 0)
      .reduce((sum, passengers) => sum + passengers)
      .value();

    // Update global stats
    this.unladenMass = unladenMass;
    this.cargoCapacity = cargoCapacity;
    this.fuelCapacity = fuelCapacity;
    this.passengerCapacity = passengerCapacity;
    this.ladenMass = unladenMass + fuelCapacity + cargoCapacity;

    return this;
  }

  /**
   * Update movement values
   * @return {this} The ship instance (for chaining operations)
   */
  updateMovement() {
    this.speeds = Calc.speed(this.unladenMass + this.fuelCapacity, this.speed, this.standard[1].m, this.pipSpeed);
    this.topSpeed = this.speeds[4];
    this.topBoost = this.canBoost(0, 0) ? this.speeds[4] * this.boost / this.speed : 0;

    this.pitches = Calc.pitch(this.unladenMass + this.fuelCapacity, this.pitch, this.standard[1].m, this.pipSpeed);
    this.topPitch = this.pitches[4];

    this.rolls = Calc.roll(this.unladenMass + this.fuelCapacity, this.roll, this.standard[1].m, this.pipSpeed);
    this.topRoll = this.rolls[4];

    this.yaws = Calc.yaw(this.unladenMass + this.fuelCapacity, this.yaw, this.standard[1].m, this.pipSpeed);
    this.topYaw = this.yaws[4];

    return this;
  }

  /**
   * Update shield
   * @return {this} The ship instance (for chaining operations)
   */
  recalculateShield() {
    // Obtain shield metrics with 0 pips to sys (parts affected by SYS aren't used here)
    const metrics = Calc.shieldMetrics(this, 0);

    this.shield = metrics.generator ? metrics.generator + metrics.boosters + metrics.addition : 0;
    this.shieldExplRes = this.shield > 0 ? 1 - metrics.explosive.total : null;
    this.shieldKinRes = this.shield > 0 ?  1 - metrics.kinetic.total : null;
    this.shieldThermRes = this.shield > 0 ?  1 - metrics.thermal.total : null;
    return this;
  }

  /**
   * Update shield cells
   * @return {this} The ship instance (for chaining operations)
   */
  recalculateShieldCells() {
    let shieldCells = 0;

    for (let slot of this.internal) {
      if (slot.m && slot.m.grp == 'scb') {
        // There is currently a bug with Elite where you can have a clip > 1 thanks to engineering but it doesn't do anything,
        // so we need to hard-code clip to 1
        shieldCells += slot.m.getShieldReinforcement() * slot.m.getDuration() * (slot.m.getAmmo() + 1);
      }
    }

    this.shieldCells = shieldCells;

    return this;
  }

  /**
   * Update armour and hull resistances
   * @return {this} The ship instance (for chaining operations)
   */
  recalculateArmour() {
    // Armour from bulkheads
    let metrics = Calc.armourMetrics(this);

    this.armour = metrics.total ? metrics.total : 0;
    this.modulearmour = metrics.modulearmour;
    this.moduleprotection = metrics.moduleprotection;
    this.hullExplRes = 1 - metrics.explosive.total;
    this.hullKinRes = 1 - metrics.kinetic.total;
    this.hullThermRes = 1 - metrics.thermal.total;
    this.hullCausRes = 1 - metrics.caustic.total;
    return this;
  }

  /**
   * Jump Range and total range calculations
   * @return {this} The ship instance (for chaining operations)
   */
  updateJumpStats() {
    let fsd = this.standard[2].m;   // Frame Shift Drive;
    let { unladenMass, fuelCapacity } = this;
    this.unladenRange = this.calcUnladenRange(); // Includes fuel weight for jump
    this.fullTankRange = Calc.jumpRange(unladenMass + fuelCapacity, fsd, fuelCapacity, this); // Full Tank
    this.ladenRange = this.calcLadenRange(); // Includes full tank and caro
    this.unladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity, fsd, fuelCapacity, this);
    this.ladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity + this.cargoCapacity, fsd, fuelCapacity, this);
    this.maxJumpCount = Math.ceil(fuelCapacity / fsd.getMaxFuelPerJump());
    return this;
  }

  /**
   * Update the serialized power priorites string
   * @return {this} The ship instance (for chaining operations)
   */
  updatePowerPrioritesString() {
    let priorities = [this.cargoHatch.priority];

    for (let slot of this.standard) {
      priorities.push(slot.priority);
    }
    for (let slot of this.hardpoints) {
      priorities.push(slot.priority);
    }
    for (let slot of this.internal) {
      priorities.push(slot.priority);
    }

    this.serialized.priorities = LZString.compressToBase64(priorities.join(''));
    return this;
  }

  /**
   * Update the serialized power active/inactive string
   * @return {this} The ship instance (for chaining operations)
   */
  updatePowerEnabledString() {
    let enabled = [this.cargoHatch.enabled ? 1 : 0];

    for (let slot of this.standard) {
      enabled.push(slot.enabled ? 1 : 0);
    }
    for (let slot of this.hardpoints) {
      enabled.push(slot.enabled ? 1 : 0);
    }
    for (let slot of this.internal) {
      enabled.push(slot.enabled ? 1 : 0);
    }

    this.serialized.enabled = LZString.compressToBase64(enabled.join(''));
    return this;
  }

  /**
   * Populate the modifications array with modification values from the code
   * @param {String} code    Serialized modification code
   * @param {Array}  arr     Modification array
   */
  decodeModificationsString(code, arr) {
    let moduleMods = code.split(',');
    for (let i = 0; i < arr.length; i++) {
      arr[i] = {};
      if (moduleMods.length > i && moduleMods[i] != '') {
        let mods = moduleMods[i].split(';');
        for (let j = 0; j < mods.length; j++) {
          let modElements = mods[j].split(':');
          if (modElements[0].match('[0-9]+')) {
            const modification = _.find(Modifications.modifications, function(o) { return o.id === modElements[0]; });
            if (modification != null) arr[i][modification.name] = Number(modElements[1]);
          } else {
            arr[i][modElements[0]] = Number(modElements[1]);
          }
        }
      }
    }
  }

  /**
   * Update the modifications string.
   * This is a binary structure.  It starts with a byte that identifies a slot, with bulkheads being ID 0 and moving through
   * standard modules, hardpoints, and finally internal modules.  It then contains one or more modifications, with each
   * modification being a one-byte modification ID and at two-byte modification value.  Modification IDs are based on the array
   * in Modifications.modifications.  The list of modifications is terminated by a modification ID of -1.  The structure then repeats
   * for the next module, and the next, and is terminated by a slot ID of -1.
   * @return {this} The ship instance (for chaining operations)
   */
  updateModificationsString() {
    // Start off by gathering the information that we need
    let slots = new Array();
    let blueprints = new Array();
    let specials = new Array();

    let bulkheadMods = new Array();
    let bulkheadBlueprint = null;
    if (this.bulkheads.m && this.bulkheads.m.mods) {
      for (let modKey in this.bulkheads.m.mods) {
        // Filter out invalid modifications
        if (Modifications.modules['bh'] && Modifications.modules['bh'].modifications.indexOf(modKey) != -1) {
          bulkheadMods.push({ id: Modifications.modifications[modKey].id, value: this.bulkheads.m.getModValue(modKey, true) });
        }
      }
      bulkheadBlueprint = this.bulkheads.m.blueprint;
    }
    slots.push(bulkheadMods);
    blueprints.push(bulkheadBlueprint);
    specials.push(bulkheadBlueprint ? bulkheadBlueprint.special : null);

    for (let slot of this.standard) {
      let slotMods = new Array();
      if (slot.m && slot.m.mods) {
        for (let modKey in slot.m.mods) {
          // Filter out invalid modifications
          if (Modifications.modules[slot.m.grp] && Modifications.modules[slot.m.grp].modifications.indexOf(modKey) != -1) {
            slotMods.push({ id: Modifications.modifications[modKey].id, value: slot.m.getModValue(modKey, true) });
          }
        }
      }
      slots.push(slotMods);
      blueprints.push(slot.m ? slot.m.blueprint : null);
      specials.push(slot.m && slot.m.blueprint ? slot.m.blueprint.special : null);
    }

    for (let slot of this.hardpoints) {
      let slotMods = new Array();
      if (slot.m && slot.m.mods) {
        for (let modKey in slot.m.mods) {
          // Filter out invalid modifications
          if (Modifications.modules[slot.m.grp] && Modifications.modules[slot.m.grp].modifications.indexOf(modKey) != -1) {
            slotMods.push({ id: Modifications.modifications[modKey].id, value: slot.m.getModValue(modKey, true) });
          }
        }
      }
      slots.push(slotMods);
      blueprints.push(slot.m ? slot.m.blueprint : null);
      specials.push(slot.m && slot.m.blueprint ? slot.m.blueprint.special : null);
    }

    for (let slot of this.internal) {
      let slotMods = new Array();
      if (slot.m && slot.m.mods) {
        for (let modKey in slot.m.mods) {
          // Filter out invalid modifications
          if (Modifications.modules[slot.m.grp] && Modifications.modules[slot.m.grp].modifications.indexOf(modKey) != -1) {
            slotMods.push({ id: Modifications.modifications[modKey].id, value: slot.m.getModValue(modKey, true) });
          }
        }
      }
      slots.push(slotMods);
      blueprints.push(slot.m ? slot.m.blueprint : null);
      specials.push(slot.m && slot.m.blueprint ? slot.m.blueprint.special : null);
    }

    // Now work out the size of the binary buffer from our modifications array
    let bufsize = 0;
    for (let i = 0; i < slots.length; i++) {
      if (slots[i].length > 0 || (blueprints[i] && blueprints[i].id)) {
        // Length is 1 for the slot ID, 5 for each modification, 1 for the end marker of the modifications and 1 for the end marker of the slot
        bufsize = bufsize + 1 + (5 * slots[i].length) + 1 + 1;

        if (blueprints[i] && blueprints[i].id) {
          // Additional 10 for the blueprint and grade
          bufsize += 10;
        }

        if (specials[i]) {
          // Additional 5 for each special
          bufsize += 5;
        }
      }
    }

    if (bufsize > 0) {
      bufsize = bufsize + 1; // For end marker
      // Now create and populate the buffer
      let buffer = Buffer.alloc(bufsize);
      let curpos = 0;
      let i = 0;
      for (let slot of slots) {
        if (slot.length > 0 || (blueprints[i] && blueprints[i].id)) {
          buffer.writeInt8(i, curpos++);
          if (blueprints[i] && blueprints[i].id) {
            buffer.writeInt8(MODIFICATION_ID_BLUEPRINT, curpos++);
            buffer.writeInt32LE(blueprints[i].id, curpos);
            curpos += 4;
            buffer.writeInt8(MODIFICATION_ID_GRADE, curpos++);
            buffer.writeInt32LE(blueprints[i].grade, curpos);
            curpos += 4;
          }
          if (specials[i]) {
            buffer.writeInt8(MODIFICATION_ID_SPECIAL, curpos++);
            buffer.writeInt32LE(specials[i].id, curpos);
            curpos += 4;
          }
          for (let slotMod of slot) {
            buffer.writeInt8(slotMod.id, curpos++);
            if (isNaN(slotMod.value)) {
              // Need to write the string with exactly four characters, so pad with whitespace
              buffer.write(('    ' + slotMod.value).slice(-4), curpos, 4);
            } else {
              buffer.writeInt32LE(slotMod.value, curpos);
            }
            const modification = _.find(Modifications.modifications, function(o) { return o.id === slotMod.id; });
            // console.log('ENCODE Slot ' + i + ': ' + modification.name + ' = ' + slotMod.value);
            curpos += 4;
          }
          buffer.writeInt8(MODIFICATION_ID_DONE, curpos++);
        }
        i++;
      }
      if (curpos > 0) {
        buffer.writeInt8(SLOT_ID_DONE, curpos++);
      }

      this.serialized.modifications = zlib.gzipSync(buffer).toString('base64');
      // console.log(this.serialized.modifications)
    } else {
      this.serialized.modifications = null;
    }
    return this;
  }

  /**
   * Populate the modifications array with modification values from the code.
   * See updateModificationsString() for details of the structure.
   * @param {String} buffer         Buffer holding modification info
   * @param {Array}  modArr         Modification array
   * @param {Array}  blueprintArr    Blueprint array
   */
  decodeModificationsStruct(buffer, modArr, blueprintArr) {
    let curpos = 0;
    let slot = buffer.readInt8(curpos++);
    while (slot != SLOT_ID_DONE) {
      let modifications = {};
      let blueprint = {};
      let modificationId = buffer.readInt8(curpos++);
      while (modificationId != MODIFICATION_ID_DONE) {
        let modificationValue;
        if (modificationId === 40) {
          // Type is special, in that it's a character string
          modificationValue = buffer.toString('utf8', curpos, curpos + 4).trim();
        } else {
          modificationValue = buffer.readInt32LE(curpos);
        }
        curpos += 4;
        // There are a number of 'special' modification IDs, check for them here
        if (modificationId === MODIFICATION_ID_BLUEPRINT) {
          if (modificationValue !== 0) {
            blueprint = Object.assign(blueprint, _.find(Modifications.blueprints, function(o) { return o.id === modificationValue; }));
          }
        } else if (modificationId === MODIFICATION_ID_GRADE) {
          if (modificationValue !== 0) {
            blueprint.grade = modificationValue;
          }
        } else if (modificationId === MODIFICATION_ID_SPECIAL) {
          blueprint.special = _.find(Modifications.specials, function(o) { return o.id === modificationValue; });
        } else {
          const modification = _.find(Modifications.modifications, function(o) { return o.id === modificationId; });
          // console.log('DECODE Slot ' + slot + ': ' + modification.name + ' = ' + modificationValue);
          modifications[modification.name] = modificationValue;
        }
        modificationId = buffer.readInt8(curpos++);
      }
      modArr[slot] = modifications;
      if (blueprint.id) {
        blueprintArr[slot] = blueprint;
      }
      slot = buffer.readInt8(curpos++);
    }
  }

  /**
   * Update a slot with a the modul if the id is different from the current id for this slot.
   * Has logic handling ModuleUtils that you may only have 1 of (Shield Generator or Refinery).
   *
   * @param {Object}  slot            The modul slot
   * @param {Object}  mdef            Properties for the selected modul
   * @param {boolean} preventUpdate   If true, do not update aggregated stats
   * @return {this} The ship instance (for chaining operations)
   */
  use(slot, mdef, preventUpdate) {
    // See if the module passed in is really a module or just a definition, and fix it accordingly so that we have a module instance
    let m;
    if (mdef == null) {
      m = null;
    } else if (mdef instanceof Module) {
      m = mdef;
    } else {
      m = new Module({ grp: mdef.grp, id: mdef.id });
    }

    if (slot.m != m) { // Selecting a different modul
      // Slot is an internal slot, is not being emptied, and the selected modul group/type must be of unique
      if (slot.cat == 2 && m && UNIQUE_MODULES.indexOf(m.grp) != -1) {
        // Find another internal slot that already has this type/group installed
        let similarSlot = this.findInternalByGroup(m.grp);
        // If another slot has an installed modul with of the same type
        if (!preventUpdate && similarSlot && similarSlot !== slot) {
          this.updateStats(similarSlot, null, similarSlot.m);
          similarSlot.m = null;  // Empty the slot
          similarSlot.discountedCost = 0;
        }
      }
      let oldModule = slot.m;
      slot.m = m;
      slot.enabled = true;
      slot.discountedCost = (m && m.cost) ? m.cost * this.moduleCostMultiplier : 0;
      this.updateStats(slot, m, oldModule, preventUpdate);

      switch (slot.cat) {
        case 0: this.serialized.standard = null; break;
        case 1: this.serialized.hardpoints = null; break;
        case 2: this.serialized.internal = null;
      }
    }
    return this;
  }

  /**
   * Mount the specified bulkhead type (index)
   * @param  {Number} index           Bulkhead index [0-4]
   * @param  {boolean} preventUpdate  Prevent summary update
   * @return {this} The ship instance (for chaining operations)
   */
  useBulkhead(index, preventUpdate) {
    let oldBulkhead = this.bulkheads.m;
    this.bulkheads.m = this.availCS.getBulkhead(index);
    this.bulkheads.discountedCost = this.bulkheads.m.cost * this.moduleCostMultiplier;
    this.updateStats(this.bulkheads, this.bulkheads.m, oldBulkhead, preventUpdate);
    this.serialized.standard = null;
    return this;
  }

  /**
   * Set all standard slots to use the speficied rating and class based on
   * the slot's max class
   * @param  {String} rating Module Rating (A-E)
   * @return {this} The ship instance (for chaining operations)
   */
  useStandard(rating) {
    for (let i = this.standard.length - 1; i--;) { // All except Fuel Tank
      let id = this.standard[i].maxClass + rating;
      this.use(this.standard[i], ModuleUtils.standard(i, id));
    }
    return this;
  }

  /**
   * Calculate the lowest possible mass for this ship.
   * @param  {Object} m Module override set (standard type => Module)
   * @return {number} The lowest possible mass for this ship
   */
  calcLowestPossibleMass(m) {
    m = m || {};

    let mass = this.hullMass;
    mass += m.pp ? m.pp.getMass() : ModuleUtils.standard(0, '2D').getMass();
    mass += m.th ? m.th.getMass() : ModuleUtils.standard(1, '2D').getMass();
    mass += m.fsd ? m.fsd.getMass() : ModuleUtils.standard(2, '2D').getMass();
    mass += m.ls ? m.ls.getMass() : ModuleUtils.standard(3, this.standard[3].maxClass + 'D').getMass() * 0.3; // Lightweight grade 4 mod reduces mass by up to 70%
    mass += m.pd ? m.pd.getMass() : ModuleUtils.standard(4, '1D').getMass();
    mass += m.s ? m.s.getMass() : ModuleUtils.standard(5, this.standard[5].maxClass + 'D').getMass() * 0.2; // Lightweight grade 5 mod reduces mass by up to 80%
    // Ignore fuel tank as it could be empty
    return mass;
  }

  /**
   * Use the lightest standard ModuleUtils unless otherwise specified
   * @param  {Object} m Module override set (standard type => module ID)
   * @return {this} The ship instance (for chaining operations)
   */
  useLightestStandard(m) {
    m = m || {};

    let standard = this.standard,
        // Find lightest Power Distributor that can still boost;
        pd = m.pd ? ModuleUtils.standard(4, m.pd) : this.availCS.lightestPowerDist(this.boostEnergy),
        fsd = ModuleUtils.standard(2, m.fsd || standard[2].maxClass + 'A'),
        ls = ModuleUtils.standard(3, m.ls || standard[3].maxClass + 'D'),
        s = ModuleUtils.standard(5, m.s || standard[5].maxClass + 'D'),
        ft = m.ft ? ModuleUtils.standard(6, m.ft) : standard[6].m, // Use existing fuel tank unless specified
        updated;

    this.useBulkhead(0)
      .use(standard[2], fsd)   // FSD
      .use(standard[3], ls)    // Life Support
      .use(standard[5], s)     // Sensors
      .use(standard[4], pd)    // Power Distributor
      .use(standard[6], ft);   // Fuel Tank

    // Turn off nearly everything
    if (m.fsdDisabled) this.setSlotEnabled(this.standard[2], false);
    if (m.pdDisabled) this.setSlotEnabled(this.standard[4], false);
    if (m.sDisabled) this.setSlotEnabled(this.standard[5], false);

    // Thrusters and Powerplant must be determined after all other ModuleUtils are mounted
    // Loop at least once to determine absolute lightest PD and TH
    do {
      updated = false;
      // Find lightest Thruster that still works for the ship at max mass
      let th = m.th ? ModuleUtils.standard(1, m.th) : this.availCS.lightestThruster(this.ladenMass);
      if (!isEqual.isEqual(th, standard[1].m)) {
        this.use(standard[1], th);
        updated = true;
      }
      // Find lightest Power plant that can power the ship
      let pp = m.pp ? ModuleUtils.standard(0, m.pp) : this.availCS.lightestPowerPlant(Math.max(this.powerRetracted, this.powerDeployed), m.ppRating);

      if (!isEqual.isEqual(pp, standard[0].m)) {
        this.use(standard[0], pp);
        updated = true;
      }
    } while (updated);

    return this;
  }

  /**
   * Fill all utility slots with the specified module
   * @param  {String} group   Group name
   * @param  {String} rating  Rating [A-I]
   * @param  {String} name    Module name
   * @param  {boolean} clobber Overwrite non-empty slots
   * @return {this} The ship instance (for chaining operations)
   */
  useUtility(group, rating, name, clobber) {
    let m = ModuleUtils.findHardpoint(group, 0, rating, name);
    for (let i = this.hardpoints.length; i--;) {
      if ((clobber || !this.hardpoints[i].m) && !this.hardpoints[i].maxClass) {
        this.use(this.hardpoints[i], m);
      }
    }
    return this;
  }

  /**
   * [useWeapon description]
   * @param  {[type]} group   [description]
   * @param  {[type]} mount   [description]
   * @param  {[type]} missile [description]
   * @param  {boolean} clobber Overwrite non-empty slots
   * @return {this} The ship instance (for chaining operations)
   */
  useWeapon(group, mount, missile, clobber) {
    let hps = this.hardpoints;
    for (let i = hps.length; i--;) {
      if (hps[i].maxClass) {
        let size = hps[i].maxClass, m;
        do {
          m = ModuleUtils.findHardpoint(group, size, null, null, mount, missile);
          if ((clobber || !hps[i].m) && m) {
            this.use(hps[i], m);
            break;
          }
        } while (!m && (--size > 0));
      }
    }
    return this;
  }

  /**
   * Upgrade information about internals with version changes
   * @param {array} internals     the internals from the ship code
   * @param {int}   offset        the offset of the internals information in the priorities etc. arrays
   * @param {array} priorities    the existing priorities arrray
   * @param {array} enableds      the existing enableds arrray
   * @param {array} modifications the existing modifications arrray
   * @param {array} blueprints    the existing blueprints arrray
   * @param {int}   version       the version of the information
   */
  upgradeInternals(internals, offset, priorities, enableds, modifications, blueprints, version) {
    if (version == 1) {
      // Version 2 reflects the addition of military slots.  this means that we need to juggle the internals and their
      // associated information around to make holes in the appropriate places
      for (let slotId = 0; slotId < this.internal.length; slotId++) {
        if (this.internal[slotId].eligible && this.internal[slotId].eligible.mrp) {
          // Found a restricted military slot - push all of the existing items down one to compensate for the fact that they didn't exist before now
          internals.push.apply(internals, [0].concat(internals.splice(slotId).slice(0, -1)));

          const offsetSlotId = offset + slotId;

          // Same for priorities etc.
          if (priorities) { priorities.push.apply(priorities, [0].concat(priorities.splice(offsetSlotId))); }
          if (enableds) { enableds.push.apply(enableds, [1].concat(enableds.splice(offsetSlotId))); }
          if (modifications) { modifications.push.apply(modifications, [null].concat(modifications.splice(offsetSlotId).slice(0, -1))); }
          if (blueprints) { blueprints.push.apply(blueprints, [null].concat(blueprints.splice(offsetSlotId).slice(0, -1))); }
        }
      }
      // Ensure that all items are the correct length
      internals.splice(Ships[this.id].slots.internal.length);
    }
  }
}
