#include "simulation.h"
#include "camera.h"
#include "common.h"
#include "hittable_list.h"
#include "sphere.h"

#include <chrono>
#include <iostream>
#include <thread>

std::atomic<bool> running(true);

/**
 * Initialize solar system.
 */
SimulationState initialize_solar_system() {
  SimulationState state;
  state.bodies.push_back(Body{"Sol", Vec3(0.0, 0.0, 0.0), Vec3(0.0, 0.0, 0.0),
                              1.989e30, 10.0}); // a star similar to the sun
  state.bodies.push_back(Body{"Mercury", Vec3(57.909e9, 0.0, 0.0),
                              Vec3(0.0, 47.36e3, 0.0), 0.33011e24,
                              1}); // a planet similar to mercury
  state.bodies.push_back(Body{"Venus", Vec3(108.209e9, 0.0, 0.0),
                              Vec3(0.0, 35.02e3, 0.0), 4.8675e24,
                              3}); // a planet similar to venus
  state.bodies.push_back(Body{"Earth", Vec3(149.596e9, 0.0, 0.0),
                              Vec3(0.0, 29.78e3, 0.0), 5.9724e24,
                              3}); // a planet similar to earth
  state.bodies.push_back(Body{"Mars", Vec3(227.923e9, 0.0, 0.0),
                              Vec3(0.0, 24.07e3, 0.0), 0.64171e24,
                              2}); // a planet similar to mars
  state.bodies.push_back(Body{"Jupiter", Vec3(778.570e9, 0.0, 0.0),
                              Vec3(0.0, 13e3, 0.0), 1898.19e24,
                              5}); // a planet similar to jupiter
  state.bodies.push_back(Body{"Saturn", Vec3(1433.529e9, 0.0, 0.0),
                              Vec3(0.0, 9.68e3, 0.0), 568.34e24,
                              4}); // a planet similar to saturn
  state.bodies.push_back(Body{"Uranus", Vec3(2872.463e9, 0.0, 0.0),
                              Vec3(0.0, 6.80e3, 0.0), 86.813e24,
                              3}); // a planet similar to uranus
  state.bodies.push_back(Body{"Neptune", Vec3(4495.060e9, 0.0, 0.0),
                              Vec3(0.0, 5.43e3, 0.0), 102.413e24,
                              3}); // a planet similar to neptune
  return state;
}

/**
 * Simulation thread.
 */
void simulation_thread(RingBuffer<SimulationState> &buffer) {
  SimulationState state = initialize_solar_system();
  float BIG_G = 6.67e-11; // gravitational constant
  float dt = 0.01; // if we didnt do this it would run too fast for render
  std::chrono::duration<float> elapsed;

  auto last = std::chrono::high_resolution_clock::now();

  while (running) {
    auto now = std::chrono::high_resolution_clock::now();
    elapsed = now - last;

    if (elapsed.count() >= dt) {
      float bdt = 86400;

      size_t n_bodies = state.bodies.size();
      for (size_t m1_idx = 0; m1_idx < n_bodies; m1_idx++) {
        Vec3 a_g;

        for (size_t m2_idx = 0; m2_idx < n_bodies; m2_idx++) {
          if (m2_idx != m1_idx) {
            Vec3 r_vector =
                state.bodies[m1_idx].position - state.bodies[m2_idx].position;
            double r_mag = r_vector.length();

            double acceleration =
                -BIG_G * state.bodies[m2_idx].mass / (r_mag * r_mag);

            Vec3 r_unit_vector = r_vector / r_mag;

            a_g += r_unit_vector * acceleration;
          }
        }

        state.bodies[m1_idx].velocity += a_g * bdt;
      }

      for (size_t entity_idx = 0; entity_idx < n_bodies; entity_idx++) {
        state.bodies[entity_idx].position +=
            state.bodies[entity_idx].velocity * bdt;
      }

      // Try to push it into the buffer
      while (!buffer.push(state)) {
        // Buffer full, drop or wait (e.g. yield or sleep)
        std::this_thread::yield();
      }

      state.tick++;
      last = now;
    } else {
      std::this_thread::sleep_for(std::chrono::milliseconds(2));
    }
  }
}

/**
 * Render thread.
 */
void render_thread(RingBuffer<SimulationState> &buffer, Canvas &canvas, Camera &cam) {
  while (running) {
    auto maybe_state = buffer.pop();
    if (maybe_state.has_value()) {
      const SimulationState &state = maybe_state.value();

      hittable_list world;

      const double SCALE = 1e9;

      for (auto b : state.bodies) {
        // we need to swap y and z because the raytracter uses opengl style
        // coordinates
        auto bb = b.position / SCALE;
        world.add(make_shared<sphere>(Vec3(bb.x(), bb.z(), bb.y()), b.radius));
      }

      cam.render(world, canvas);
    } else {
      std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
  }
}
