#include "particle.h"

#ifdef SHR3D_PARTICLE

#include "geometry.h"
#include "global.h"
#include "opengl.h"
#include "shader.h"
#include "type.h"

#include <algorithm>

static f32 rand0To1()
{
  return f32(rand()) / f32(RAND_MAX);
  //return f32(lfsr()) / f32(U16::max);
}

static u64 particleSpawnThisFrame(const f32 spawnsPerSecond)
{
  u64 particlesToSpawnThisFrame;
  const f32 particlesToSpawnThisFrameF32 = spawnsPerSecond * TimeNS_To_Seconds(Global::frameDelta);
  particlesToSpawnThisFrame = u64(particlesToSpawnThisFrameF32);
  // particlesToSpawnThisFrameF32 could be lower an 1.0.
  if (rand0To1() < fmodf(particlesToSpawnThisFrameF32, 1.0))
    ++particlesToSpawnThisFrame;

  return particlesToSpawnThisFrame;
}

void Particle::highwayFretboardSpawn(const HighwaySpawn& highwaySpawn)
{
  ASSERT(Settings::highwayParticlePlayedNotes || Settings::highwayParticleCollisionNotes);

  const u64 particlesToSpawn = particleSpawnThisFrame(Settings::highwayParticleSpawnsPerSecond * highwaySpawn.spawnMultiplier);

  if (particlesToSpawn == 0)
    return;

  i32 particlesAdded = 0;

  for (i32 i = 0; i < Settings::highwayParticleMaxCount; ++i)
  {
    if (Global::particleDeathTime[i] > Global::time_)
      continue;

    Global::particleDeathTime[i] = Global::time_ + timeNS_From_Seconds(Settings::highwayParticleMinLifeTime) + timeNS_From_Seconds(rand0To1() * (Settings::highwayParticleMaxLifeTime - Settings::highwayParticleMinLifeTime));

    Global::particle[i].particleBornTime = Global::time_;

    { // position
      //const f32 angle = rand0To1() * 2 * PI_;
      //const f32 centerDistance = rand0To1();
      //const f32 pointInsideCircle_x = centerDistance * Settings::highwayParticleSpawnRadius * cos(angle);
      //const f32 pointInsideCircle_y = centerDistance * Settings::highwayParticleSpawnRadius * sin(angle);
      //particle[i].bornPosition.x = highwaySpawn.position.x + pointInsideCircle_x;
      //particle[i].bornPosition.y = highwaySpawn.position.y + pointInsideCircle_y;
      //particle[i].bornPosition.z = highwaySpawn.position.z;

      Global::particle[i].bornPosition.x = highwaySpawn.position.x + Settings::highwayParticleSpawnRadius * (0.5f - rand0To1()) * highwaySpawn.width;
      Global::particle[i].bornPosition.y = highwaySpawn.position.y + Settings::highwayParticleSpawnRadius * (0.5f - rand0To1()) * highwaySpawn.height;
      Global::particle[i].bornPosition.z = highwaySpawn.position.z;
    }

    { // velocity
      Global::particle[i].velocity.x = Settings::highwayParticleMinVelocityX + rand0To1() * (Settings::highwayParticleMaxVelocityX - Settings::highwayParticleMinVelocityX);
      Global::particle[i].velocity.y = Settings::highwayParticleMinVelocityY + rand0To1() * (Settings::highwayParticleMaxVelocityY - Settings::highwayParticleMinVelocityY);
      Global::particle[i].velocity.z = Settings::highwayParticleMinVelocityZ + rand0To1() * (Settings::highwayParticleMaxVelocityZ - Settings::highwayParticleMinVelocityZ);
    }

    { // acceleration
      Global::particle[i].acceleration.x = Settings::highwayParticleAccelerationX;
      Global::particle[i].acceleration.y = Settings::highwayParticleAccelerationY;
      Global::particle[i].acceleration.z = Settings::highwayParticleAccelerationZ;
    }

    { // scale
      Global::particle[i].scale = Settings::highwayParticleMinSize + rand0To1() * (Settings::highwayParticleMaxSize - Settings::highwayParticleMinSize);
    }

    { // rotatiion
      Global::particle[i].rotation = Settings::highwayParticleMinRotationAngle + rand0To1() * (Settings::highwayParticleMaxRotationAngle - Settings::highwayParticleMinRotationAngle);
    }

    { // color
      Global::particle[i].color = highwaySpawn.color;
      if (Settings::highwayParticleColorVariation != 0.0)
      {
        Global::particle[i].color.r += Settings::highwayParticleColorVariation * (0.5f - rand0To1());
        Global::particle[i].color.g += Settings::highwayParticleColorVariation * (0.5f - rand0To1());
        Global::particle[i].color.b += Settings::highwayParticleColorVariation * (0.5f - rand0To1());
        Global::particle[i].color.r = clamp(Global::particle[i].color.r, 0.0f, 1.0f);
        Global::particle[i].color.g = clamp(Global::particle[i].color.g, 0.0f, 1.0f);
        Global::particle[i].color.b = clamp(Global::particle[i].color.b, 0.0f, 1.0f);
      }
    }

    ++particlesAdded;
    if (particlesAdded == particlesToSpawn)
      break;
  }
}

struct GpuData
{
  mat4 mvp;
  vec4 color;
};

static bool sortZDrawOrder(const GpuData& g0, const GpuData& g1)
{
  return g0.mvp.m23 > g1.mvp.m23;
}

void Particle::render(const mat4& highwayViewProjectionMat)
{
  ASSERT(Settings::highwayParticlePlayedNotes || Settings::highwayParticleCollisionNotes);

  if (Global::selectedSongIndex == -1)
    return;

  u32 aliveParticleCount = 0;
  static GpuData gpuData[Const::highwayParticleMaxCount];
  //GpuData* gpuData = reinterpret_cast<GpuData*>(alloca(sizeof(GpuData) * Const::highwayParticleMaxCount)); // if this crashes, the stack size must be increased

  for (i32 i = 0; i < Settings::highwayParticleMaxCount; ++i)
  {
    if (Global::particleDeathTime[i] < Global::time_)
    {
      gpuData[i].mvp.m23 = -F32::max;
      continue;
    }
    ++aliveParticleCount;

    const TimeNS aliveTime = Global::time_ - Global::particle[i].particleBornTime;
    ASSERT(aliveTime >= 0);
    const f32 aliveTimeF32 = TimeNS_To_Seconds(aliveTime);

    const TimeNS totalAliveTime = Global::particleDeathTime[i] - Global::particle[i].particleBornTime;
    const TimeNS remainingTime = Global::particleDeathTime[i] - Global::time_;
    const f32 normalizedLifeRemaining = f32(remainingTime) / f32(totalAliveTime);
    ASSERT(normalizedLifeRemaining >= 0.0f);
    ASSERT(normalizedLifeRemaining <= 1.0f);

    const f32 sizeFactor = smoothstep(0.0, 0.10f, normalizedLifeRemaining) - smoothstep(0.75, 1.0f, normalizedLifeRemaining); // shrink particle when it is going to die

    const mat4 scale = { // scale mat
      .m00 = Global::particle[i].scale * sizeFactor,
      .m11 = Global::particle[i].scale * sizeFactor,
      .m22 = Global::particle[i].scale * sizeFactor
    };

    const mat4 rotationMat = { // rotation mat
      .m00 = cosf(Global::particle[i].rotation),
      .m10 = sinf(Global::particle[i].rotation),
      .m01 = -sinf(Global::particle[i].rotation),
      .m11 = cosf(Global::particle[i].rotation)
    };

    mat4 model = vec::multiply(rotationMat, scale);

    { // model position
      const f32 aliveTimeF32Cubed = aliveTimeF32 * aliveTimeF32;
      model.m03 = Global::particle[i].bornPosition.x + aliveTimeF32 * Global::particle[i].velocity.x + aliveTimeF32Cubed * Global::particle[i].acceleration.x;
      model.m13 = Global::particle[i].bornPosition.y + aliveTimeF32 * Global::particle[i].velocity.y + aliveTimeF32Cubed * Global::particle[i].acceleration.y;
      model.m23 = Global::particle[i].bornPosition.z + aliveTimeF32 * Global::particle[i].velocity.z + aliveTimeF32Cubed * Global::particle[i].acceleration.z;
    }

    gpuData[i].mvp = vec::multiply(highwayViewProjectionMat, model);

    { // color
      gpuData[i].color.r = Global::particle[i].color.r;
      gpuData[i].color.g = Global::particle[i].color.g;
      gpuData[i].color.b = Global::particle[i].color.b;
      gpuData[i].color.a = Global::particle[i].color.a;
    }
  }

  if (aliveParticleCount == 0)
    return;

  std::sort(gpuData, &gpuData[Settings::highwayParticleMaxCount], sortZDrawOrder);

#ifndef NDEBUG
  { // make sure particles are sorted correctly
    f32 z = F32::max;
    for (u32 i = 0; i < aliveParticleCount; ++i)
    {
      ASSERT(z >= gpuData[i].mvp.m23);
      z = gpuData[i].mvp.m23;
    }
  }
#endif // NDEBUG

  GL(glUseProgram(Shader::particleInstanced));

  GLuint instanceVBO;
  GL(glGenBuffers(1, &instanceVBO));
  GL(glBindBuffer(GL_ARRAY_BUFFER, instanceVBO));
  GL(glBufferData(GL_ARRAY_BUFFER, aliveParticleCount * 20 * sizeof(float), &gpuData[0], GL_STATIC_DRAW));

  GL(glEnableVertexAttribArray(2));
  GL(glEnableVertexAttribArray(3));
  GL(glEnableVertexAttribArray(4));
  GL(glEnableVertexAttribArray(5));
  GL(glEnableVertexAttribArray(6));
  GL(glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 20 * sizeof(float), nullptr));
  GL(glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 20 * sizeof(float), (void*)(4 * sizeof(GLfloat))));
  GL(glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 20 * sizeof(float), (void*)(8 * sizeof(GLfloat))));
  GL(glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 20 * sizeof(float), (void*)(12 * sizeof(GLfloat))));
  GL(glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 20 * sizeof(float), (void*)(16 * sizeof(GLfloat))));
  GL(glVertexAttribDivisor(2, 1));
  GL(glVertexAttribDivisor(3, 1));
  GL(glVertexAttribDivisor(4, 1));
  GL(glVertexAttribDivisor(5, 1));
  GL(glVertexAttribDivisor(6, 1));
  switch (Settings::highwayParticleShape)
  {
  case 0:
    GL(glDrawElementsInstancedBaseVertex_compat(particle0, aliveParticleCount));
    break;
  case 1:
    GL(glDrawElementsInstancedBaseVertex_compat(particle1, aliveParticleCount));
    break;
  case 2:
    GL(glDrawElementsInstancedBaseVertex_compat(particle2, aliveParticleCount));
    break;
  default:
    unreachable();
  }
  GL(glVertexAttribDivisor(2, 0));
  GL(glVertexAttribDivisor(3, 0));
  GL(glVertexAttribDivisor(4, 0));
  GL(glVertexAttribDivisor(5, 0));
  GL(glVertexAttribDivisor(6, 0));
  GL(glDisableVertexAttribArray(2));
  GL(glDisableVertexAttribArray(3));
  GL(glDisableVertexAttribArray(4));
  GL(glDisableVertexAttribArray(5));
  GL(glDisableVertexAttribArray(6));

  GL(glDeleteBuffers(1, &instanceVBO));

  GL(glBindBuffer(GL_ARRAY_BUFFER, Global::vbo));
}

#endif // SHR3D_PARTICLE
