import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import Color from "../Core/Color.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import Event from "../Core/Event.js";
import JulianDate from "../Core/JulianDate.js";
import CesiumMath from "../Core/Math.js";
import Matrix4 from "../Core/Matrix4.js";
import BillboardCollection from "./BillboardCollection.js";
import CircleEmitter from "./CircleEmitter.js";
import Particle from "./Particle.js";

var defaultImageSize = new Cartesian2(1.0, 1.0);

/**
 * A ParticleSystem manages the updating and display of a collection of particles.
 *
 * @alias ParticleSystem
 * @constructor
 *
 * @param {Object} [options] Object with the following properties:
 * @param {Boolean} [options.show=true] Whether to display the particle system.
 * @param {ParticleSystem.updateCallback} [options.updateCallback] The callback function to be called each frame to update a particle.
 * @param {ParticleEmitter} [options.emitter=new CircleEmitter(0.5)] The particle emitter for this system.
 * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms the particle system from model to world coordinates.
 * @param {Matrix4} [options.emitterModelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms the particle system emitter within the particle systems local coordinate system.
 * @param {Number} [options.emissionRate=5] The number of particles to emit per second.
 * @param {ParticleBurst[]} [options.bursts] An array of {@link ParticleBurst}, emitting bursts of particles at periodic times.
 * @param {Boolean} [options.loop=true] Whether the particle system should loop its bursts when it is complete.
 * @param {Number} [options.scale=1.0] Sets the scale to apply to the image of the particle for the duration of its particleLife.
 * @param {Number} [options.startScale] The initial scale to apply to the image of the particle at the beginning of its life.
 * @param {Number} [options.endScale] The final scale to apply to the image of the particle at the end of its life.
 * @param {Color} [options.color=Color.WHITE] Sets the color of a particle for the duration of its particleLife.
 * @param {Color} [options.startColor] The color of the particle at the beginning of its life.
 * @param {Color} [options.endColor] The color of the particle at the end of its life.
 * @param {Object} [options.image] The URI, HTMLImageElement, or HTMLCanvasElement to use for the billboard.
 * @param {Cartesian2} [options.imageSize=new Cartesian2(1.0, 1.0)] If set, overrides the minimumImageSize and maximumImageSize inputs that scale the particle image's dimensions in pixels.
 * @param {Cartesian2} [options.minimumImageSize] Sets the minimum bound, width by height, above which to randomly scale the particle image's dimensions in pixels.
 * @param {Cartesian2} [options.maximumImageSize] Sets the maximum bound, width by height, below which to randomly scale the particle image's dimensions in pixels.
 * @param {Boolean} [options.sizeInMeters] Sets if the size of particles is in meters or pixels. <code>true</code> to size the particles in meters; otherwise, the size is in pixels.
 * @param {Number} [options.speed=1.0] If set, overrides the minimumSpeed and maximumSpeed inputs with this value.
 * @param {Number} [options.minimumSpeed] Sets the minimum bound in meters per second above which a particle's actual speed will be randomly chosen.
 * @param {Number} [options.maximumSpeed] Sets the maximum bound in meters per second below which a particle's actual speed will be randomly chosen.
 * @param {Number} [options.lifetime=Number.MAX_VALUE] How long the particle system will emit particles, in seconds.
 * @param {Number} [options.particleLife=5.0] If set, overrides the minimumParticleLife and maximumParticleLife inputs with this value.
 * @param {Number} [options.minimumParticleLife] Sets the minimum bound in seconds for the possible duration of a particle's life above which a particle's actual life will be randomly chosen.
 * @param {Number} [options.maximumParticleLife] Sets the maximum bound in seconds for the possible duration of a particle's life below which a particle's actual life will be randomly chosen.
 * @param {Number} [options.mass=1.0] Sets the minimum and maximum mass of particles in kilograms.
 * @param {Number} [options.minimumMass] Sets the minimum bound for the mass of a particle in kilograms. A particle's actual mass will be chosen as a random amount above this value.
 * @param {Number} [options.maximumMass] Sets the maximum mass of particles in kilograms. A particle's actual mass will be chosen as a random amount below this value.
 * @demo {@link https://cesium.com/learn/cesiumjs-learn/cesiumjs-particle-systems/|Particle Systems Tutorial}
 * @demo {@link https://sandcastle.cesium.com/?src=Particle%20System.html&label=Showcases|Particle Systems Tutorial Demo}
 * @demo {@link https://sandcastle.cesium.com/?src=Particle%20System%20Fireworks.html&label=Showcases|Particle Systems Fireworks Demo}
 */
function ParticleSystem(options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  /**
   * Whether to display the particle system.
   * @type {Boolean}
   * @default true
   */
  this.show = defaultValue(options.show, true);

  /**
   * An array of force callbacks. The callback is passed a {@link Particle} and the difference from the last time
   * @type {ParticleSystem.updateCallback}
   * @default undefined
   */
  this.updateCallback = options.updateCallback;

  /**
   * Whether the particle system should loop it's bursts when it is complete.
   * @type {Boolean}
   * @default true
   */
  this.loop = defaultValue(options.loop, true);

  /**
   * The URI, HTMLImageElement, or HTMLCanvasElement to use for the billboard.
   * @type {Object}
   * @default undefined
   */
  this.image = defaultValue(options.image, undefined);

  var emitter = options.emitter;
  if (!defined(emitter)) {
    emitter = new CircleEmitter(0.5);
  }
  this._emitter = emitter;

  this._bursts = options.bursts;

  this._modelMatrix = Matrix4.clone(
    defaultValue(options.modelMatrix, Matrix4.IDENTITY)
  );
  this._emitterModelMatrix = Matrix4.clone(
    defaultValue(options.emitterModelMatrix, Matrix4.IDENTITY)
  );
  this._matrixDirty = true;
  this._combinedMatrix = new Matrix4();

  this._startColor = Color.clone(
    defaultValue(options.color, defaultValue(options.startColor, Color.WHITE))
  );
  this._endColor = Color.clone(
    defaultValue(options.color, defaultValue(options.endColor, Color.WHITE))
  );

  this._startScale = defaultValue(
    options.scale,
    defaultValue(options.startScale, 1.0)
  );
  this._endScale = defaultValue(
    options.scale,
    defaultValue(options.endScale, 1.0)
  );

  this._emissionRate = defaultValue(options.emissionRate, 5.0);

  this._minimumSpeed = defaultValue(
    options.speed,
    defaultValue(options.minimumSpeed, 1.0)
  );
  this._maximumSpeed = defaultValue(
    options.speed,
    defaultValue(options.maximumSpeed, 1.0)
  );

  this._minimumParticleLife = defaultValue(
    options.particleLife,
    defaultValue(options.minimumParticleLife, 5.0)
  );
  this._maximumParticleLife = defaultValue(
    options.particleLife,
    defaultValue(options.maximumParticleLife, 5.0)
  );

  this._minimumMass = defaultValue(
    options.mass,
    defaultValue(options.minimumMass, 1.0)
  );
  this._maximumMass = defaultValue(
    options.mass,
    defaultValue(options.maximumMass, 1.0)
  );

  this._minimumImageSize = Cartesian2.clone(
    defaultValue(
      options.imageSize,
      defaultValue(options.minimumImageSize, defaultImageSize)
    )
  );
  this._maximumImageSize = Cartesian2.clone(
    defaultValue(
      options.imageSize,
      defaultValue(options.maximumImageSize, defaultImageSize)
    )
  );

  this._sizeInMeters = defaultValue(options.sizeInMeters, false);

  this._lifetime = defaultValue(options.lifetime, Number.MAX_VALUE);

  this._billboardCollection = undefined;
  this._particles = [];

  // An array of available particles that we can reuse instead of allocating new.
  this._particlePool = [];

  this._previousTime = undefined;
  this._currentTime = 0.0;
  this._carryOver = 0.0;

  this._complete = new Event();
  this._isComplete = false;

  this._updateParticlePool = true;
  this._particleEstimate = 0;
}

Object.defineProperties(ParticleSystem.prototype, {
  /**
   * The particle emitter for this
   * @memberof ParticleSystem.prototype
   * @type {ParticleEmitter}
   * @default CircleEmitter
   */
  emitter: {
    get: function () {
      return this._emitter;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.defined("value", value);
      //>>includeEnd('debug');
      this._emitter = value;
    },
  },
  /**
   * An array of {@link ParticleBurst}, emitting bursts of particles at periodic times.
   * @memberof ParticleSystem.prototype
   * @type {ParticleBurst[]}
   * @default undefined
   */
  bursts: {
    get: function () {
      return this._bursts;
    },
    set: function (value) {
      this._bursts = value;
      this._updateParticlePool = true;
    },
  },
  /**
   * The 4x4 transformation matrix that transforms the particle system from model to world coordinates.
   * @memberof ParticleSystem.prototype
   * @type {Matrix4}
   * @default Matrix4.IDENTITY
   */
  modelMatrix: {
    get: function () {
      return this._modelMatrix;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.defined("value", value);
      //>>includeEnd('debug');
      this._matrixDirty =
        this._matrixDirty || !Matrix4.equals(this._modelMatrix, value);
      Matrix4.clone(value, this._modelMatrix);
    },
  },
  /**
   * The 4x4 transformation matrix that transforms the particle system emitter within the particle systems local coordinate system.
   * @memberof ParticleSystem.prototype
   * @type {Matrix4}
   * @default Matrix4.IDENTITY
   */
  emitterModelMatrix: {
    get: function () {
      return this._emitterModelMatrix;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.defined("value", value);
      //>>includeEnd('debug');
      this._matrixDirty =
        this._matrixDirty || !Matrix4.equals(this._emitterModelMatrix, value);
      Matrix4.clone(value, this._emitterModelMatrix);
    },
  },
  /**
   * The color of the particle at the beginning of its life.
   * @memberof ParticleSystem.prototype
   * @type {Color}
   * @default Color.WHITE
   */
  startColor: {
    get: function () {
      return this._startColor;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.defined("value", value);
      //>>includeEnd('debug');
      Color.clone(value, this._startColor);
    },
  },
  /**
   * The color of the particle at the end of its life.
   * @memberof ParticleSystem.prototype
   * @type {Color}
   * @default Color.WHITE
   */
  endColor: {
    get: function () {
      return this._endColor;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.defined("value", value);
      //>>includeEnd('debug');
      Color.clone(value, this._endColor);
    },
  },
  /**
   * The initial scale to apply to the image of the particle at the beginning of its life.
   * @memberof ParticleSystem.prototype
   * @type {Number}
   * @default 1.0
   */
  startScale: {
    get: function () {
      return this._startScale;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.number.greaterThanOrEquals("value", value, 0.0);
      //>>includeEnd('debug');
      this._startScale = value;
    },
  },
  /**
   * The final scale to apply to the image of the particle at the end of its life.
   * @memberof ParticleSystem.prototype
   * @type {Number}
   * @default 1.0
   */
  endScale: {
    get: function () {
      return this._endScale;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.number.greaterThanOrEquals("value", value, 0.0);
      //>>includeEnd('debug');
      this._endScale = value;
    },
  },
  /**
   * The number of particles to emit per second.
   * @memberof ParticleSystem.prototype
   * @type {Number}
   * @default 5
   */
  emissionRate: {
    get: function () {
      return this._emissionRate;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.number.greaterThanOrEquals("value", value, 0.0);
      //>>includeEnd('debug');
      this._emissionRate = value;
      this._updateParticlePool = true;
    },
  },
  /**
   * Sets the minimum bound in meters per second above which a particle's actual speed will be randomly chosen.
   * @memberof ParticleSystem.prototype
   * @type {Number}
   * @default 1.0
   */
  minimumSpeed: {
    get: function () {
      return this._minimumSpeed;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.number.greaterThanOrEquals("value", value, 0.0);
      //>>includeEnd('debug');
      this._minimumSpeed = value;
    },
  },
  /**
   * Sets the maximum bound in meters per second below which a particle's actual speed will be randomly chosen.
   * @memberof ParticleSystem.prototype
   * @type {Number}
   * @default 1.0
   */
  maximumSpeed: {
    get: function () {
      return this._maximumSpeed;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.number.greaterThanOrEquals("value", value, 0.0);
      //>>includeEnd('debug');
      this._maximumSpeed = value;
    },
  },
  /**
   * Sets the minimum bound in seconds for the possible duration of a particle's life above which a particle's actual life will be randomly chosen.
   * @memberof ParticleSystem.prototype
   * @type {Number}
   * @default 5.0
   */
  minimumParticleLife: {
    get: function () {
      return this._minimumParticleLife;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.number.greaterThanOrEquals("value", value, 0.0);
      //>>includeEnd('debug');
      this._minimumParticleLife = value;
    },
  },
  /**
   * Sets the maximum bound in seconds for the possible duration of a particle's life below which a particle's actual life will be randomly chosen.
   * @memberof ParticleSystem.prototype
   * @type {Number}
   * @default 5.0
   */
  maximumParticleLife: {
    get: function () {
      return this._maximumParticleLife;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.number.greaterThanOrEquals("value", value, 0.0);
      //>>includeEnd('debug');
      this._maximumParticleLife = value;
      this._updateParticlePool = true;
    },
  },
  /**
   * Sets the minimum mass of particles in kilograms.
   * @memberof ParticleSystem.prototype
   * @type {Number}
   * @default 1.0
   */
  minimumMass: {
    get: function () {
      return this._minimumMass;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.number.greaterThanOrEquals("value", value, 0.0);
      //>>includeEnd('debug');
      this._minimumMass = value;
    },
  },
  /**
   * Sets the maximum mass of particles in kilograms.
   * @memberof ParticleSystem.prototype
   * @type {Number}
   * @default 1.0
   */
  maximumMass: {
    get: function () {
      return this._maximumMass;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.number.greaterThanOrEquals("value", value, 0.0);
      //>>includeEnd('debug');
      this._maximumMass = value;
    },
  },
  /**
   * Sets the minimum bound, width by height, above which to randomly scale the particle image's dimensions in pixels.
   * @memberof ParticleSystem.prototype
   * @type {Cartesian2}
   * @default new Cartesian2(1.0, 1.0)
   */
  minimumImageSize: {
    get: function () {
      return this._minimumImageSize;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.object("value", value);
      Check.typeOf.number.greaterThanOrEquals("value.x", value.x, 0.0);
      Check.typeOf.number.greaterThanOrEquals("value.y", value.y, 0.0);
      //>>includeEnd('debug');
      this._minimumImageSize = value;
    },
  },
  /**
   * Sets the maximum bound, width by height, below which to randomly scale the particle image's dimensions in pixels.
   * @memberof ParticleSystem.prototype
   * @type {Cartesian2}
   * @default new Cartesian2(1.0, 1.0)
   */
  maximumImageSize: {
    get: function () {
      return this._maximumImageSize;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.object("value", value);
      Check.typeOf.number.greaterThanOrEquals("value.x", value.x, 0.0);
      Check.typeOf.number.greaterThanOrEquals("value.y", value.y, 0.0);
      //>>includeEnd('debug');
      this._maximumImageSize = value;
    },
  },
  /**
   * Gets or sets if the particle size is in meters or pixels. <code>true</code> to size particles in meters; otherwise, the size is in pixels.
   * @memberof ParticleSystem.prototype
   * @type {Boolean}
   * @default false
   */
  sizeInMeters: {
    get: function () {
      return this._sizeInMeters;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.bool("value", value);
      //>>includeEnd('debug');
      this._sizeInMeters = value;
    },
  },
  /**
   * How long the particle system will emit particles, in seconds.
   * @memberof ParticleSystem.prototype
   * @type {Number}
   * @default Number.MAX_VALUE
   */
  lifetime: {
    get: function () {
      return this._lifetime;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.number.greaterThanOrEquals("value", value, 0.0);
      //>>includeEnd('debug');
      this._lifetime = value;
    },
  },
  /**
   * Fires an event when the particle system has reached the end of its lifetime.
   * @memberof ParticleSystem.prototype
   * @type {Event}
   */
  complete: {
    get: function () {
      return this._complete;
    },
  },
  /**
   * When <code>true</code>, the particle system has reached the end of its lifetime; <code>false</code> otherwise.
   * @memberof ParticleSystem.prototype
   * @type {Boolean}
   */
  isComplete: {
    get: function () {
      return this._isComplete;
    },
  },
});

function updateParticlePool(system) {
  var emissionRate = system._emissionRate;
  var life = system._maximumParticleLife;

  var burstAmount = 0;
  var bursts = system._bursts;
  if (defined(bursts)) {
    var length = bursts.length;
    for (var i = 0; i < length; ++i) {
      burstAmount += bursts[i].maximum;
    }
  }

  var billboardCollection = system._billboardCollection;
  var image = system.image;

  var particleEstimate = Math.ceil(emissionRate * life + burstAmount);
  var particles = system._particles;
  var particlePool = system._particlePool;
  var numToAdd = Math.max(
    particleEstimate - particles.length - particlePool.length,
    0
  );

  for (var j = 0; j < numToAdd; ++j) {
    var particle = new Particle();
    particle._billboard = billboardCollection.add({
      image: image,
    });
    particlePool.push(particle);
  }

  system._particleEstimate = particleEstimate;
}

function getOrCreateParticle(system) {
  // Try to reuse an existing particle from the pool.
  var particle = system._particlePool.pop();
  if (!defined(particle)) {
    // Create a new one
    particle = new Particle();
  }
  return particle;
}

function addParticleToPool(system, particle) {
  system._particlePool.push(particle);
}

function freeParticlePool(system) {
  var particles = system._particles;
  var particlePool = system._particlePool;
  var billboardCollection = system._billboardCollection;

  var numParticles = particles.length;
  var numInPool = particlePool.length;
  var estimate = system._particleEstimate;

  var start = numInPool - Math.max(estimate - numParticles - numInPool, 0);
  for (var i = start; i < numInPool; ++i) {
    var p = particlePool[i];
    billboardCollection.remove(p._billboard);
  }
  particlePool.length = start;
}

function removeBillboard(particle) {
  if (defined(particle._billboard)) {
    particle._billboard.show = false;
  }
}

function updateBillboard(system, particle) {
  var billboard = particle._billboard;
  if (!defined(billboard)) {
    billboard = particle._billboard = system._billboardCollection.add({
      image: particle.image,
    });
  }
  billboard.width = particle.imageSize.x;
  billboard.height = particle.imageSize.y;
  billboard.position = particle.position;
  billboard.sizeInMeters = system.sizeInMeters;
  billboard.show = true;

  // Update the color
  var r = CesiumMath.lerp(
    particle.startColor.red,
    particle.endColor.red,
    particle.normalizedAge
  );
  var g = CesiumMath.lerp(
    particle.startColor.green,
    particle.endColor.green,
    particle.normalizedAge
  );
  var b = CesiumMath.lerp(
    particle.startColor.blue,
    particle.endColor.blue,
    particle.normalizedAge
  );
  var a = CesiumMath.lerp(
    particle.startColor.alpha,
    particle.endColor.alpha,
    particle.normalizedAge
  );
  billboard.color = new Color(r, g, b, a);

  // Update the scale
  billboard.scale = CesiumMath.lerp(
    particle.startScale,
    particle.endScale,
    particle.normalizedAge
  );
}

function addParticle(system, particle) {
  particle.startColor = Color.clone(system._startColor, particle.startColor);
  particle.endColor = Color.clone(system._endColor, particle.endColor);
  particle.startScale = system._startScale;
  particle.endScale = system._endScale;
  particle.image = system.image;
  particle.life = CesiumMath.randomBetween(
    system._minimumParticleLife,
    system._maximumParticleLife
  );
  particle.mass = CesiumMath.randomBetween(
    system._minimumMass,
    system._maximumMass
  );
  particle.imageSize.x = CesiumMath.randomBetween(
    system._minimumImageSize.x,
    system._maximumImageSize.x
  );
  particle.imageSize.y = CesiumMath.randomBetween(
    system._minimumImageSize.y,
    system._maximumImageSize.y
  );

  // Reset the normalizedAge and age in case the particle was reused.
  particle._normalizedAge = 0.0;
  particle._age = 0.0;

  var speed = CesiumMath.randomBetween(
    system._minimumSpeed,
    system._maximumSpeed
  );
  Cartesian3.multiplyByScalar(particle.velocity, speed, particle.velocity);

  system._particles.push(particle);
}

function calculateNumberToEmit(system, dt) {
  // This emitter is finished if it exceeds it's lifetime.
  if (system._isComplete) {
    return 0;
  }

  dt = CesiumMath.mod(dt, system._lifetime);

  // Compute the number of particles to emit based on the emissionRate.
  var v = dt * system._emissionRate;
  var numToEmit = Math.floor(v);
  system._carryOver += v - numToEmit;
  if (system._carryOver > 1.0) {
    numToEmit++;
    system._carryOver -= 1.0;
  }

  // Apply any bursts
  if (defined(system.bursts)) {
    var length = system.bursts.length;
    for (var i = 0; i < length; i++) {
      var burst = system.bursts[i];
      var currentTime = system._currentTime;
      if (defined(burst) && !burst._complete && currentTime > burst.time) {
        numToEmit += CesiumMath.randomBetween(burst.minimum, burst.maximum);
        burst._complete = true;
      }
    }
  }

  return numToEmit;
}

var rotatedVelocityScratch = new Cartesian3();

/**
 * @private
 */
ParticleSystem.prototype.update = function (frameState) {
  if (!this.show) {
    return;
  }

  if (!defined(this._billboardCollection)) {
    this._billboardCollection = new BillboardCollection();
  }

  if (this._updateParticlePool) {
    updateParticlePool(this);
    this._updateParticlePool = false;
  }

  // Compute the frame time
  var dt = 0.0;
  if (this._previousTime) {
    dt = JulianDate.secondsDifference(frameState.time, this._previousTime);
  }

  if (dt < 0.0) {
    dt = 0.0;
  }

  var particles = this._particles;
  var emitter = this._emitter;
  var updateCallback = this.updateCallback;

  var i;
  var particle;

  // update particles and remove dead particles
  var length = particles.length;
  for (i = 0; i < length; ++i) {
    particle = particles[i];
    if (!particle.update(dt, updateCallback)) {
      removeBillboard(particle);
      // Add the particle back to the pool so it can be reused.
      addParticleToPool(this, particle);
      particles[i] = particles[length - 1];
      --i;
      --length;
    } else {
      updateBillboard(this, particle);
    }
  }
  particles.length = length;

  var numToEmit = calculateNumberToEmit(this, dt);

  if (numToEmit > 0 && defined(emitter)) {
    // Compute the final model matrix by combining the particle systems model matrix and the emitter matrix.
    if (this._matrixDirty) {
      this._combinedMatrix = Matrix4.multiply(
        this.modelMatrix,
        this.emitterModelMatrix,
        this._combinedMatrix
      );
      this._matrixDirty = false;
    }

    var combinedMatrix = this._combinedMatrix;

    for (i = 0; i < numToEmit; i++) {
      // Create a new particle.
      particle = getOrCreateParticle(this);

      // Let the emitter initialize the particle.
      this._emitter.emit(particle);

      //For the velocity we need to add it to the original position and then multiply by point.
      Cartesian3.add(
        particle.position,
        particle.velocity,
        rotatedVelocityScratch
      );
      Matrix4.multiplyByPoint(
        combinedMatrix,
        rotatedVelocityScratch,
        rotatedVelocityScratch
      );

      // Change the position to be in world coordinates
      particle.position = Matrix4.multiplyByPoint(
        combinedMatrix,
        particle.position,
        particle.position
      );

      // Orient the velocity in world space as well.
      Cartesian3.subtract(
        rotatedVelocityScratch,
        particle.position,
        particle.velocity
      );
      Cartesian3.normalize(particle.velocity, particle.velocity);

      // Add the particle to the system.
      addParticle(this, particle);
      updateBillboard(this, particle);
    }
  }

  this._billboardCollection.update(frameState);
  this._previousTime = JulianDate.clone(frameState.time, this._previousTime);
  this._currentTime += dt;

  if (
    this._lifetime !== Number.MAX_VALUE &&
    this._currentTime > this._lifetime
  ) {
    if (this.loop) {
      this._currentTime = CesiumMath.mod(this._currentTime, this._lifetime);
      if (this.bursts) {
        var burstLength = this.bursts.length;
        // Reset any bursts
        for (i = 0; i < burstLength; i++) {
          this.bursts[i]._complete = false;
        }
      }
    } else {
      this._isComplete = true;
      this._complete.raiseEvent(this);
    }
  }

  // free particles in the pool and release billboard GPU memory
  if (frameState.frameNumber % 120 === 0) {
    freeParticlePool(this);
  }
};

/**
 * Returns true if this object was destroyed; otherwise, false.
 * <br /><br />
 * If this object was destroyed, it should not be used; calling any function other than
 * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
 *
 * @returns {Boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
 *
 * @see ParticleSystem#destroy
 */
ParticleSystem.prototype.isDestroyed = function () {
  return false;
};

/**
 * Destroys the WebGL resources held by this object.  Destroying an object allows for deterministic
 * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
 * <br /><br />
 * Once an object is destroyed, it should not be used; calling any function other than
 * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.  Therefore,
 * assign the return value (<code>undefined</code>) to the object as done in the example.
 *
 * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
 *
 * @see ParticleSystem#isDestroyed
 */
ParticleSystem.prototype.destroy = function () {
  this._billboardCollection =
    this._billboardCollection && this._billboardCollection.destroy();
  return destroyObject(this);
};

/**
 * A function used to modify attributes of the particle at each time step. This can include force modifications,
 * color, sizing, etc.
 *
 * @callback ParticleSystem.updateCallback
 *
 * @param {Particle} particle The particle being updated.
 * @param {Number} dt The time in seconds since the last update.
 *
 * @example
 * function applyGravity(particle, dt) {
 *    var position = particle.position;
 *    var gravityVector = Cesium.Cartesian3.normalize(position, new Cesium.Cartesian3());
 *    Cesium.Cartesian3.multiplyByScalar(gravityVector, GRAVITATIONAL_CONSTANT * dt, gravityVector);
 *    particle.velocity = Cesium.Cartesian3.add(particle.velocity, gravityVector, particle.velocity);
 * }
 */
export default ParticleSystem;
