const NUM_PLANE_POSITIONS = 12;

const BLENDINGS = {
  normal: THREE.NormalBlending,
  additive: THREE.AdditiveBlending,
  substractive: THREE.SubstractiveBlending,
  multiply: THREE.MultiplyBlending
};

const SHADERS = {
  flat: THREE.MeshBasicMaterial,
  lambert: THREE.MeshLambertMaterial,
  phong: THREE.MeshPhongMaterial,
  standard: THREE.MeshStandardMaterial
};

const OFFSCREEN_VEC3 = new THREE.Vector3(-99999, -99999, -99999);

/**
 * Particle Player component for A-Frame.
 */
AFRAME.registerComponent('particleplayer', {
  schema: {
    blending: {
      default: 'additive',
      oneOf: ['normal', 'additive', 'multiply', 'substractive']
    },
    color: {default: '#fff', type: 'color'},
    count: {default: '100%'},
    delay: {default: 0, type: 'int'},
    dur: {default: 1000, type: 'int'},
    img: {type: 'selector'},
    interpolate: {default: false},
    loop: {default: 'false'},
    on: {default: 'init'},
    poolSize: {default: 5, type: 'int'}, // number of simultaneous particle systems
    protation: {type: 'vec3'},
    pscale: {default: 1.0, type: 'float'},
    scale: {default: 1.0, type: 'float'},
    initialScale: {type: 'vec3'},
    finalScale: {type: 'vec3'},
    animateScale: {default: false},
    shader: {
      default: 'flat',
      oneOf: ['flat', 'lambert', 'phong', 'standard']
    },
    src: {type: 'selector'}
  },

  multiple: true,

  init: function () {
    this.frame = 0;
    this.framedata = null;
    this.indexPool = null;
    this.lastFrame = 0;
    this.material = null;
    this.msPerFrame = 0;
    this.numFrames = 0;
    this.numParticles = 0; // total number of particles per system
    this.originalVertexPositions = [];
    this.particleCount = 0; // actual number of particles to spawn per event (data.count)
    this.particleSystems = [];
    this.protation = false;
    this.restPositions = []; // position at first frame each particle is alive
    this.restRotations = [];
    this.sprite_rotation = false;
    this.systems = null;
    this.useRotation = false;
    this.scaleAnim = new THREE.Vector3();
  },

  update: function (oldData) {
    const data = this.data;

    if (!data.src) {
      return;
    }

    if (oldData.on !== data.on) {
      if (oldData.on) {
        this.el.removeEventListener(oldData.on, this.start);
      }
      if (data.on !== 'play') {
        this.el.addEventListener(data.on, this.start.bind(this));
      }
    }

    this.loadParticlesJSON(data.src, data.scale);

    this.numFrames = this.framedata.length;
    this.numParticles = this.numFrames > 0 ? this.framedata[0].length : 0;

    if (data.count[data.count.length - 1] === '%') {
      this.particleCount = Math.floor(
        (parseInt(data.count) * this.numParticles) / 100.0
      );
    } else {
      this.particleCount = parseInt(data.count);
    }
    this.particleCount = Math.min(
      this.numParticles,
      Math.max(0, this.particleCount)
    );

    this.msPerFrame = data.dur / this.numFrames;

    this.indexPool = new Array(this.numParticles);

    const materialParams = {
      color: new THREE.Color(data.color),
      side: THREE.DoubleSide,
      blending: BLENDINGS[data.blending],
      map: data.img ? new THREE.TextureLoader().load(data.img.src) : null,
      depthWrite: false,
      opacity: data.opacity,
      transparent: !!data.img || data.blending !== 'normal' || data.opacity < 1
    };
    if (SHADERS[data.shader] !== undefined) {
      this.material = new SHADERS[data.shader](materialParams);
    } else {
      this.material = new SHADERS['flat'](materialParams);
    }

    this.createParticles(data.poolSize);

    if (data.on === 'init') {
      this.start();
    }
  },

  loadParticlesJSON: function (json, scale) {
    var alive;

    this.restPositions.length = 0;
    this.restRotations.length = 0;

    const jsonData = JSON.parse(json.data);
    const frames = jsonData.frames;
    const precision = jsonData.precision;

    this.useRotation = jsonData.rotation;

    if (jsonData.sprite_rotation !== false) {
      this.sprite_rotation = {
        x: jsonData.sprite_rotation[0] / precision,
        y: jsonData.sprite_rotation[1] / precision,
        z: jsonData.sprite_rotation[2] / precision
      };
    } else {
      this.sprite_rotation = false;
    }

    this.framedata = new Array(frames.length);
    for (let frameIndex = 0; frameIndex < frames.length; frameIndex++) {
      this.framedata[frameIndex] = new Array(frames[frameIndex].length);
      for (
        let particleIndex = 0;
        particleIndex < frames[frameIndex].length;
        particleIndex++
      ) {
        let rawP = frames[frameIndex][particleIndex]; // data of particle i in frame f
        alive = rawP !== 0; // 0 means not alive yet this frame.

        let p = (this.framedata[frameIndex][particleIndex] = {
          position: alive
            ? {
              x: (rawP[0] / precision) * scale,
              y: (rawP[1] / precision) * scale,
              z: (rawP[2] / precision) * scale
            }
            : null,
          alive: alive
        });

        if (jsonData.rotation) {
          p.rotation = alive
            ? {
              x: rawP[3] / precision,
              y: rawP[4] / precision,
              z: rawP[5] / precision
            }
            : null;
        }

        if (alive && frameIndex === 0) {
          this.restPositions[particleIndex] = p.position
            ? {x: p.position.y, y: p.position.y, z: p.position.z}
            : null;
          this.restRotations[particleIndex] = p.rotation
            ? {x: p.rotation.y, y: p.rotation.y, z: p.rotation.z}
            : null;
        }
      }
    }
  },

  createParticles: (function () {
    const tempGeometries = [];

    return function (numParticleSystems) {
      const data = this.data;
      var loop = parseInt(this.data.loop);

      this.particleSystems.length = 0;

      if (isNaN(loop)) {
        loop = this.data.loop === 'true' ? Number.MAX_VALUE : 0;
      }

      for (let i = 0; i < numParticleSystems; i++) {
        let particleSystem = {
          active: false,
          activeParticleIndices: new Array(this.particleCount),
          loopCount: 0,
          loopTotal: loop,
          mesh: null,
          time: 0,
          pscale: data.animateScale ? new THREE.Vector3() : null
        };

        // Fill array of geometries to merge.
        const ratio = data.img ? data.img.width / data.img.height : 1;
        tempGeometries.length = 0;
        for (let p = 0; p < this.numParticles; p++) {
          let geometry = new THREE.PlaneBufferGeometry(
            0.1 * ratio * data.pscale,
            0.1 * data.pscale
          );
          if (this.sprite_rotation !== false) {
            geometry.rotateX(this.sprite_rotation.x);
            geometry.rotateY(this.sprite_rotation.y);
            geometry.rotateZ(this.sprite_rotation.z);
          } else {
            geometry.rotateX((this.data.protation.x * Math.PI) / 180);
            geometry.rotateY((this.data.protation.y * Math.PI) / 180);
            geometry.rotateZ((this.data.protation.z * Math.PI) / 180);
          }
          tempGeometries.push(geometry);
        }

        // Create merged geometry for whole particle system.
        let mergedBufferGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries(
          tempGeometries
        );

        particleSystem.mesh = new THREE.Mesh(
          mergedBufferGeometry,
          this.material
        );
        particleSystem.mesh.visible = false;
        this.el.setObject3D(`particleplayer${i}`, particleSystem.mesh);
        copyArray(
          this.originalVertexPositions,
          mergedBufferGeometry.attributes.position.array
        );

        // Hide all particles by default.
        for (
          let i = 0;
          i < mergedBufferGeometry.attributes.position.array.length;
          i++
        ) {
          mergedBufferGeometry.attributes.position.array[i] = -99999;
        }

        for (let i = 0; i < particleSystem.activeParticleIndices.length; i++) {
          particleSystem.activeParticleIndices[i] = i;
        }

        this.particleSystems.push(particleSystem);
      }
    };
  })(),

  start: function (evt) {
    if (this.data.delay > 0) {
      setTimeout(() => this.startAfterDelay(evt), this.data.delay);
    } else {
      this.startAfterDelay(evt);
    }
  },

  startAfterDelay: function (evt) {
    // position, rotation
    var found = -1;
    var particleSystem;
    var oldestTime = 0;
    var position = evt ? evt.detail.position : null;
    var rotation = evt ? evt.detail.rotation : null;

    if (!(position instanceof THREE.Vector3)) {
      position = new THREE.Vector3();
    }
    if (!(rotation instanceof THREE.Euler)) {
      rotation = new THREE.Euler();
    }

    // find available (or oldest) particle system
    for (var i = 0; i < this.particleSystems.length; i++) {
      if (this.particleSystems[i].active === false) {
        found = i;
        break;
      }
      if (this.particleSystems[i].time > oldestTime) {
        found = i;
        oldestTime = this.particleSystems[i].time;
      }
    }

    if (found === -1) { return; }

    particleSystem = this.particleSystems[found];
    particleSystem.active = true;
    particleSystem.loopCount = 1;
    particleSystem.mesh.visible = true;
    particleSystem.mesh.position.copy(position);
    particleSystem.mesh.rotation.copy(rotation);
    particleSystem.time = 0;
    if (this.data.animateScale) {
      particleSystem.pscale.copy(this.data.initialScale);
    }

    this.resetParticles(particleSystem);
  },

  doLoop: function (particleSystem) {
    particleSystem.loopCount++;
    particleSystem.frame = -1;
    particleSystem.time = 0;
    if (this.data.animateScale) {
      particleSystem.pscale.copy(data.initialScale);
    }
    this.resetParticles(particleSystem);
  },

  resetParticle: function (particleSystem, particleIndex) {
    const geometry = particleSystem.mesh.geometry;

    if (this.restPositions[particleIndex]) {
      transformPlane(
        particleIndex,
        geometry,
        this.originalVertexPositions,
        this.restPositions[particleIndex],
        this.useRotation && this.restRotations[particleIndex],
        particleSystem.pscale
      );
    } else {
      // Hide.
      transformPlane(
        particleIndex,
        geometry,
        this.originalVertexPositions,
        OFFSCREEN_VEC3,
        undefined,
        null
      );
    }

    // TODO: Can update transformPlane for lookAt.
    // lookAt does not support rotated or translated parents! :_(
    // part.lookAt(this.camera.position);
  },

  /**
   * When starting or finishing (looping) animation, this resets particles
   * to their initial position and, if user asked for replaying less than 100%
   * of particles, randomly choose them.
   */
  resetParticles: function (particleSystem) {
    var i;
    var rand;

    // no picking, just hide and reset
    if (this.particleCount === this.numParticles) {
      for (i = 0; i < this.numParticles; i++) {
        this.resetParticle(particleSystem, i);
      }
      return;
    }

    // hide particles from last animation and initialize indexPool
    const geometry = particleSystem.mesh.geometry;
    for (i = 0; i < this.numParticles; i++) {
      if (i < this.particleCount) {
        transformPlane(
          particleSystem.activeParticleIndices[i],
          geometry,
          this.originalVertexPositions,
          OFFSCREEN_VEC3,
          undefined,
          null
        );
      }
      this.indexPool[i] = i;
    }

    // scramble indexPool
    for (i = 0; i < this.particleCount; i++) {
      rand = i + Math.floor(Math.random() * (this.numParticles - i));
      particleSystem.activeParticleIndices[i] = this.indexPool[rand];
      this.indexPool[rand] = this.indexPool[i];
      this.resetParticle(
        particleSystem,
        particleSystem.activeParticleIndices[i]
      );
    }
  },

  tick: (function () {
    const helperPositionVec3 = new THREE.Vector3();

    return function (time, delta) {
      var frame; // current particle system frame
      var fdata; // all particles data in current frame
      var fdataNext; // next frame (for interpolation)
      var useRotation = this.useRotation;
      var frameTime; // time in current frame (for interpolation)
      var relTime; // current particle system relative time (0-1)
      var interpolate; // whether interpolate between frames or not

      for (
        let particleSystemIndex = 0;
        particleSystemIndex < this.particleSystems.length;
        particleSystemIndex++
      ) {
        let particleSystem = this.particleSystems[particleSystemIndex];
        if (!particleSystem.active) {
          continue;
        }

        // if the duration is so short that there's no need to interpolate, don't do it
        // even if user asked for it.
        interpolate =
          this.data.interpolate && this.data.dur / this.numFrames > delta;
        relTime = particleSystem.time / this.data.dur;
        frame = relTime * this.numFrames;
        fdata = this.framedata[Math.floor(frame)];
        if (interpolate) {
          frameTime = frame - Math.floor(frame);
          fdataNext =
            frame < this.numFrames - 1
              ? this.framedata[Math.floor(frame) + 1]
              : null;
        }

        if (this.data.animateScale) {
          particleSystem.pscale.lerp(this.data.finalScale, relTime);
        }

        for (
          let activeParticleIndex = 0;
          activeParticleIndex < particleSystem.activeParticleIndices.length;
          activeParticleIndex++
        ) {
          let particleIndex =
            particleSystem.activeParticleIndices[activeParticleIndex];
          let rotation = useRotation && fdata[particleIndex].rotation;

          // TODO: Add vertex position to original position to all vertices of plane...
          if (!fdata[particleIndex].alive) {
            // Hide plane off-screen when not alive.
            transformPlane(
              particleIndex,
              particleSystem.mesh.geometry,
              this.originalVertexPositions,
              OFFSCREEN_VEC3,
              undefined,
              null
            );
            continue;
          }

          if (interpolate && fdataNext && fdataNext[particleIndex].alive) {
            helperPositionVec3.lerpVectors(
              fdata[particleIndex].position,
              fdataNext[particleIndex].position,
              frameTime
            );
            transformPlane(
              particleIndex,
              particleSystem.mesh.geometry,
              this.originalVertexPositions,
              helperPositionVec3,
              rotation,
              particleSystem.pscale
            );
          } else {
            transformPlane(
              particleIndex,
              particleSystem.mesh.geometry,
              this.originalVertexPositions,
              fdata[particleIndex].position,
              rotation,
              particleSystem.pscale
            );
          }
        }

        particleSystem.time += delta;
        if (particleSystem.time >= this.data.dur) {
          if (particleSystem.loopCount < particleSystem.loopTotal) {
            this.el.emit('particleplayerloop', null, false);
            this.doLoop(particleSystem);
          } else {
            this.el.emit('particleplayerfinished', null, false);
            particleSystem.active = false;
            particleSystem.mesh.visible = false;
          }
          continue;
        }
      }
    };
  })(),

  _transformPlane: transformPlane
});

// Use triangle geometry as a helper for rotating.
const tri = (function () {
  const tri = new THREE.Geometry();
  tri.vertices.push(new THREE.Vector3());
  tri.vertices.push(new THREE.Vector3());
  tri.vertices.push(new THREE.Vector3());
  tri.faces.push(new THREE.Face3(0, 1, 2));
  return tri;
})();

/**
 * Faces of a plane are v0, v2, v1 and v2, v3, v1.
 * Positions are 12 numbers: [v0, v1, v2, v3].
 */
function transformPlane (
  particleIndex,
  geometry,
  originalArray,
  position,
  rotation,
  scale
) {
  const array = geometry.attributes.position.array;
  const index = particleIndex * NUM_PLANE_POSITIONS;

  // Calculate first face (0, 2, 1).
  tri.vertices[0].set(
    originalArray[index + 0],
    originalArray[index + 1],
    originalArray[index + 2]
  );
  tri.vertices[1].set(
    originalArray[index + 3],
    originalArray[index + 4],
    originalArray[index + 5]
  );
  tri.vertices[2].set(
    originalArray[index + 6],
    originalArray[index + 7],
    originalArray[index + 8]
  );
  if (scale !== null) {
    tri.scale(scale.x, scale.y, scale.z);
  }
  if (rotation) {
    tri.rotateX(rotation.x);
    tri.rotateY(rotation.y);
    tri.rotateZ(rotation.z);
  }
  tri.vertices[0].add(position);
  tri.vertices[1].add(position);
  tri.vertices[2].add(position);
  array[index + 0] = tri.vertices[0].x;
  array[index + 1] = tri.vertices[0].y;
  array[index + 2] = tri.vertices[0].z;
  array[index + 3] = tri.vertices[1].x;
  array[index + 4] = tri.vertices[1].y;
  array[index + 5] = tri.vertices[1].z;
  array[index + 6] = tri.vertices[2].x;
  array[index + 7] = tri.vertices[2].y;
  array[index + 8] = tri.vertices[2].z;

  // Calculate second face (2, 3, 1) just for the last vertex.
  tri.vertices[0].set(
    originalArray[index + 3],
    originalArray[index + 4],
    originalArray[index + 5]
  );
  tri.vertices[1].set(
    originalArray[index + 6],
    originalArray[index + 7],
    originalArray[index + 8]
  );
  tri.vertices[2].set(
    originalArray[index + 9],
    originalArray[index + 10],
    originalArray[index + 11]
  );
  if (scale !== null) {
    tri.scale(scale.x, scale.y, scale.z);
  }
  if (rotation) {
    tri.rotateX(rotation.x);
    tri.rotateY(rotation.y);
    tri.rotateZ(rotation.z);
  }
  tri.vertices[0].add(position);
  tri.vertices[1].add(position);
  tri.vertices[2].add(position);
  array[index + 9] = tri.vertices[2].x;
  array[index + 10] = tri.vertices[2].y;
  array[index + 11] = tri.vertices[2].z;

  geometry.attributes.position.needsUpdate = true;
}
module.exports.transformPlane = transformPlane;

function copyArray (dest, src) {
  dest.length = 0;
  for (let i = 0; i < src.length; i++) {
    dest[i] = src[i];
  }
}
