import 'dart:math' as math;
import 'dart:typed_data';

import 'package:vector_math/vector_math_64.dart';

main() {
  runTest();
}

const int numSystems = 1000;
const int numFrames = 1000;

void runTest() {
  int timeStart;
  timeStart = new DateTime.now().millisecondsSinceEpoch;

  // Create systems
  List<TestParticleSystem> systems = [];
  for (int i = 0; i < numSystems; i++) {
    systems.add(new TestParticleSystem());
  }

  int timeAfterCreate = new DateTime.now().millisecondsSinceEpoch;
  print("TIME creation ${(timeAfterCreate - timeStart) / 1000.0}");
  timeStart =  new DateTime.now().millisecondsSinceEpoch;

  // Update systems
  for (int frame = 0; frame < numFrames; frame++) {
    for (int i = 0; i < numSystems; i++) {
      systems[i].update(1.0 / 60.0);
    }
  }

  int timeAfterUpdates = new DateTime.now().millisecondsSinceEpoch;
  print("TIME updates ${(timeAfterUpdates - timeStart) / 1000.0}");
  timeStart =  new DateTime.now().millisecondsSinceEpoch;

  // Calculate matrices
  for (int frame = 0; frame < numFrames; frame++) {
    for (int i = 0; i < numSystems; i++) {
      systems[i].paint();
    }
  }

  int timeAfterMatrices = new DateTime.now().millisecondsSinceEpoch;
  print("TIME matrices ${(timeAfterMatrices - timeStart) / 1000.0}");
}

class TestParticle {
  Vector2 pos;
  Vector2 startPos;

  double colorPos;
  double deltaColorPos;

  double size;
  double deltaSize;

  double rotation;
  double deltaRotation;

  double timeToLive;

  Vector2 dir;

  double radialAccel;
  double tangentialAccel;

  Float64List simpleColorSequence;

  Matrix4 transform;
}

class TestParticleSystem {
  double life;
  double lifeVar;

  Vector2 posVar;

  double startSize;
  double startSizeVar;

  double endSize;
  double endSizeVar;

  double startRotation;
  double startRotationVar;

  double endRotation;
  double endRotationVar;

  double direction;
  double directionVar;

  double speed;
  double speedVar;

  double radialAcceleration;
  double radialAccelerationVar;

  double tangentialAcceleration;
  double tangentialAccelerationVar;

  Vector2 gravity;

  int maxParticles;
  int numParticlesToEmit;
  double emissionRate;

  List<TestParticle> _particles;

  double _emitCounter;
  int _numEmittedParticles = 0;

  TestParticleSystem({this.life: 1.5,
                  this.lifeVar: 0.0,
                  this.startSize: 2.5,
                  this.startSizeVar: 0.5,
                  this.endSize: 0.0,
                  this.endSizeVar: 0.0,
                  this.startRotation: 0.0,
                  this.startRotationVar: 0.0,
                  this.endRotation: 0.0,
                  this.endRotationVar: 0.0,
                  this.direction: 0.0,
                  this.directionVar: 360.0,
                  this.speed: 100.0,
                  this.speedVar: 50.0,
                  this.radialAcceleration: 0.0,
                  this.radialAccelerationVar: 0.0,
                  this.tangentialAcceleration: 0.0,
                  this.tangentialAccelerationVar: 0.0,
                  this.gravity,
                  this.maxParticles: 100,
                  this.emissionRate: 50.0,
                  this.numParticlesToEmit: 0}) {
    posVar = new Vector2.zero();
    _particles = new List<TestParticle>();
    _emitCounter = 0.0;
    gravity = new Vector2.zero();
  }

  void update(double dt) {
    // Create new particles
    double rate = 1.0 / emissionRate;

    if (_particles.length < maxParticles) {
      _emitCounter += dt;
    }

    while(_particles.length < maxParticles
       && _emitCounter > rate
       && (numParticlesToEmit == 0 || _numEmittedParticles < numParticlesToEmit)) {
      // Add a new particle
      _addParticle();
      _emitCounter -= rate;
    }

    // Iterate over all particles
    for (int i = _particles.length -1; i >= 0; i--) {
      TestParticle particle = _particles[i];

      // Manage life time
      particle.timeToLive -= dt;
      if (particle.timeToLive <= 0) {
        _particles.removeAt(i);
        continue;
      }

      // Update the particle

      // Radial acceleration
      Vector2 radial;
      if (particle.pos[0] != 0 || particle.pos[1] != 0) {
        radial = new Vector2.copy(particle.pos).normalize();
      } else {
        radial = new Vector2.zero();
      }
      Vector2 tangential = new Vector2.copy(radial);
      radial.scale(particle.radialAccel);

      // Tangential acceleration
      double newY = tangential.x;
      tangential.x = -tangential.y;
      tangential.y = newY;
      tangential.scale(particle.tangentialAccel);

      // (gravity + radial + tangential) * dt
      Vector2 accel = (gravity + radial + tangential).scale(dt);
      particle.dir += accel;

      // Update particle position
      particle.pos[0] += particle.dir[0] * dt;
      particle.pos[1] += particle.dir[1] * dt;

      // Size
      particle.size = math.max(particle.size + particle.deltaSize * dt, 0.0);

      // Angle
      particle.rotation += particle.deltaRotation * dt;

      // Color
      if (particle.simpleColorSequence != null) {
        for (int i = 0; i < 4; i++) {
          particle.simpleColorSequence[i] += particle.simpleColorSequence[i + 4] * dt;
        }
      } else {
        particle.colorPos = math.min(particle.colorPos + particle.deltaColorPos * dt, 1.0);
      }
    }
  }

  void _addParticle() {

    TestParticle particle = new TestParticle();

    // Time to live
    particle.timeToLive = math.max(life + lifeVar * randomSignedDouble(), 0.0);

    // Position
    Vector2 srcPos = new Vector2.zero();
    particle.pos = new Vector2(srcPos.x + posVar.x * randomSignedDouble(),
                               srcPos.y + posVar.y * randomSignedDouble());

    // Size
    particle.size = math.max(startSize + startSizeVar * randomSignedDouble(), 0.0);
    double endSizeFinal = math.max(endSize + endSizeVar * randomSignedDouble(), 0.0);
    particle.deltaSize = (endSizeFinal - particle.size) / particle.timeToLive;

    // Rotation
    particle.rotation = startRotation + startRotationVar * randomSignedDouble();
    double endRotationFinal = endRotation + endRotationVar * randomSignedDouble();
    particle.deltaRotation = (endRotationFinal - particle.rotation) / particle.timeToLive;

    // Direction
    double dirRadians = radians(direction + directionVar * randomSignedDouble());
    Vector2 dirVector = new Vector2(math.cos(dirRadians), math.sin(dirRadians));
    double speedFinal = speed + speedVar * randomSignedDouble();
    particle.dir = dirVector.scale(speedFinal);

    // Radial acceleration
    particle.radialAccel = radialAcceleration + radialAccelerationVar * randomSignedDouble();

    // Tangential acceleration
    particle.tangentialAccel = tangentialAcceleration + tangentialAccelerationVar * randomSignedDouble();

    // Colors
    particle.simpleColorSequence = new Float64List(8);
    particle.simpleColorSequence[0] = 255.0;
    particle.simpleColorSequence[1] = 255.0;
    particle.simpleColorSequence[2] = 255.0;
    particle.simpleColorSequence[3] = 255.0;

    particle.simpleColorSequence[4] = 255.0;
    particle.simpleColorSequence[5] = 0.0;
    particle.simpleColorSequence[6] = 0.0;
    particle.simpleColorSequence[7] = 0.0;

    // Transform
    particle.transform = new Matrix4.identity();

    // Add particle
    _particles.add(particle);
    _numEmittedParticles++;
  }


  void paint() {

    if (!printed) {
      printed = true;
    }

    for (int i = _particles.length -1; i >= 0; i--) {
      TestParticle particle = _particles[i];
      particle.rotation + randomSignedDouble();

      // Transform
      double c = math.cos(radians(particle.rotation));
      double s = math.sin(radians(particle.rotation));

      // Create transformation matrix for scale, position and rotation
      Matrix4 matrix = new Matrix4(c * particle.size, s * particle.size, 0.0, 0.0,
                 -s * particle.size, c * particle.size, 0.0, 0.0,
                 0.0, 0.0, 1.0, 0.0,
                particle.pos.x, particle.pos.y, 0.0, 1.0);

      particle.transform.multiply(matrix);
    }
  }
}

math.Random _random = new math.Random();

bool printed = false;

// Random methods

double randomDouble() {
  return _random.nextDouble();
}

double randomSignedDouble() {
  return _random.nextDouble() * 2.0 - 1.0;
}

int randomInt(int max) {
  return _random.nextInt(max);
}
