// solarcube.cc - SolarCube renderer for ArtCube
// Stars + Sun + orbiting planets rendered across a 6-face 64x64 cube strip.
// Face order matches globe:
// 0=TOP(+Y), 1=RIGHT(+X), 2=REAR(-Z), 3=LEFT(-X), 4=FRONT(+Z), 5=BOTTOM(-Y)
//
// Build: link with cube_orientation.cc and rgbmatrix.

#include <chrono>
#include <cmath>
#include <csignal>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <random>
#include <thread>
#include <vector>
#include <deque>

#include "led-matrix.h"
#include "cube_orientation.h"

using rgb_matrix::Canvas;
using rgb_matrix::FrameCanvas;
using rgb_matrix::RGBMatrix;

static volatile std::sig_atomic_t g_running = 1;
static void InterruptHandler(int) { g_running = 0; }

static inline float clampf(float v, float lo, float hi) {
  return v < lo ? lo : (v > hi ? hi : v);
}

static inline uint8_t dim_u8(uint8_t v, float f) {
  int x = int(float(v) * f);
  if (x < 0) x = 0;
  if (x > 255) x = 255;
  return (uint8_t)x;
}

struct Vec3 { float x, y, z; };

static inline Vec3 vadd(Vec3 a, Vec3 b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
static inline Vec3 vmul(Vec3 a, float s) { return {a.x * s, a.y * s, a.z * s}; }
static inline float vdot(Vec3 a, Vec3 b) { return a.x*b.x + a.y*b.y + a.z*b.z; }
static inline Vec3 vcross(Vec3 a, Vec3 b) {
  return {a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x};
}
static inline float vlen(Vec3 a) { return std::sqrt(vdot(a,a)); }
static inline Vec3 vnorm(Vec3 a) {
  float L = vlen(a);
  if (L <= 1e-6f) return {0,0,1};
  return {a.x/L, a.y/L, a.z/L};
}
static inline Vec3 rotate_y(Vec3 v, float a) {
  float c = std::cos(a), s = std::sin(a);
  return {v.x*c + v.z*s, v.y, -v.x*s + v.z*c};
}
static inline Vec3 rotate_x(Vec3 v, float a) {
  float c = std::cos(a), s = std::sin(a);
  return {v.x, v.y*c - v.z*s, v.y*s + v.z*c};
}
static inline Vec3 rotate_z(Vec3 v, float a) {
  float c = std::cos(a), s = std::sin(a);
  return {v.x*c - v.y*s, v.x*s + v.y*c, v.z};
}

struct FaceXY { int face, x, y; };

// Map 3D direction to cube face + N×N pixel coordinates.
static FaceXY DirToFaceXY(Vec3 d, int N) {
  d = vnorm(d);
  float ax = std::fabs(d.x), ay = std::fabs(d.y), az = std::fabs(d.z);

  int face = 4;
  float u = 0.0f, v = 0.0f;

  if (ay >= ax && ay >= az) {
    if (d.y >= 0) {
      face = 0;  // +Y TOP
      u = -d.x / ay;
      v = -d.z / ay;    // down on TOP goes toward -Z (fixes top-face handedness)
    } else {
      face = 5;  // -Y BOTTOM
      u = d.x / ay;     // bottom: keep left/right consistent with side faces
      v = -d.z / ay;    // down on BOTTOM goes toward -Z (matches top-face fix)
    }
  } else if (ax >= ay && ax >= az) {
    if (d.x >= 0) {
      face = 1;  // +X RIGHT
      u = -d.z / ax;    // right goes toward -Z
      v = -d.y / ax;    // down goes toward -Y
    } else {
      face = 3;  // -X LEFT
      u = d.z / ax;     // right goes toward +Z
      v = -d.y / ax;
    }
  } else {
    if (d.z >= 0) {
      face = 4;  // +Z FRONT
      u = d.x / az;
      v = -d.y / az;
    } else {
      face = 2;  // -Z REAR
      u = -d.x / az;
      v = -d.y / az;
    }
  }

  int px = int((u + 1.0f) * 0.5f * float(N - 1) + 0.5f);
  int py = int((v + 1.0f) * 0.5f * float(N - 1) + 0.5f);
  if (px < 0) px = 0; else if (px >= N) px = N - 1;
  if (py < 0) py = 0; else if (py >= N) py = N - 1;
  return {face, px, py};
}

static inline void PutPixel(Canvas* draw, int face, int x, int y,
                            uint8_t r, uint8_t g, uint8_t b, int N) {
  draw->SetPixel(face * N + x, y, r, g, b);
}

static void DrawStarField(Canvas* draw, const std::vector<Vec3>& stars, float t, float yaw_rad, int N) {
  const float a = t * 0.08f;
  const float b = t * 0.03f;

  for (size_t i = 0; i < stars.size(); i++) {
    Vec3 s = rotate_y(stars[i], a);
    s = rotate_z(s, b);

    float tw = 0.55f + 0.45f * std::sin(t * 1.7f + float(i) * 0.17f);
    uint8_t v = (uint8_t)clampf(18.0f + 55.0f * tw, 0.0f, 90.0f);

        s = rotate_y(s, yaw_rad);

    s = rotate_y(s, yaw_rad);

    FaceXY p = DirToFaceXY(s, N);
    PutPixel(draw, p.face, p.x, p.y, v, v, v, N);
  }
}

static void DrawStarFieldNear(Canvas* draw, const std::vector<Vec3>& stars, float t, float yaw_rad, int N) {
  const float a = t * 0.14f;
  const float b = t * 0.06f;

  for (size_t i = 0; i < stars.size(); i++) {
    Vec3 s = rotate_y(stars[i], a);
    s = rotate_z(s, b);
    s = rotate_y(s, yaw_rad);

    float tw = 0.65f + 0.35f * std::sin(t * 1.1f + float(i) * 0.23f);
    uint8_t v = (uint8_t)clampf(28.0f + 80.0f * tw, 0.0f, 140.0f);

    FaceXY p = DirToFaceXY(s, N);
    PutPixel(draw, p.face, p.x, p.y, v, v, v, N);
  }
}



static void DrawDiscOnSurface(Canvas* draw, Vec3 center_dir, float radius_px, float yaw_rad,
                              uint8_t r, uint8_t g, uint8_t b, int N) {
  center_dir = rotate_y(center_dir, yaw_rad);
  Vec3 c = vnorm(center_dir);
  Vec3 up = (std::fabs(c.y) < 0.9f) ? Vec3{0,1,0} : Vec3{0,0,1};
  Vec3 t1 = vnorm(vcross(up, c));
  Vec3 t2 = vnorm(vcross(c, t1));

  const float k = 0.022f;  // px->sphere scale

  int R = (int)radius_px;
  for (int dy = -R; dy <= R; dy++) {
    for (int dx = -R; dx <= R; dx++) {
      if (dx*dx + dy*dy > R*R) continue;
      Vec3 p = vadd(c, vadd(vmul(t1, dx * k), vmul(t2, dy * k)));
      FaceXY q = DirToFaceXY(p, N);
      PutPixel(draw, q.face, q.x, q.y, r, g, b, N);
    }
  }
}



static void DrawSaturnRing(Canvas* draw, Vec3 center_dir, float radius_px, float yaw_rad, int N) {
  // Simple ring hint: sparse points around an inclined ellipse.
  center_dir = rotate_y(center_dir, yaw_rad);
  Vec3 c = vnorm(center_dir);
  Vec3 up = (std::fabs(c.y) < 0.9f) ? Vec3{0,1,0} : Vec3{0,0,1};
  Vec3 t1 = vnorm(vcross(up, c));
  Vec3 t2 = vnorm(vcross(c, t1));

  const float k = 0.022f;
  const float a = radius_px * 2.2f;  // major axis
  const float b = radius_px * 1.2f;  // minor axis
  const float tilt = 0.55f;

  for (int i = 0; i < 60; i++) {
    float th = float(i) * (2.0f * 3.1415926f / 60.0f);
    float ex = std::cos(th) * a;
    float ey = std::sin(th) * b;

    float ty = ey * std::cos(tilt);
    float tx = ex;

    Vec3 p = vadd(c, vadd(vmul(t1, tx * k), vmul(t2, ty * k)));
    FaceXY q = DirToFaceXY(p, N);
    PutPixel(draw, q.face, q.x, q.y, 160, 150, 110, N);
  }
}

struct Planet {
  const char* name;
  float speed;
  float tilt_x;
  float tilt_z;
  float phase;
  float radius_px;
  uint8_t r, g, b;
  float distance;
};

int main(int argc, char** argv) {
  std::signal(SIGINT, InterruptHandler);
  std::signal(SIGTERM, InterruptHandler);

  RGBMatrix::Options options;
  rgb_matrix::RuntimeOptions runtime_opt;
  options.rows = 64;
  options.cols = 64;
  options.chain_length = 6;
  options.parallel = 1;
  options.hardware_mapping = "adafruit-hat";
  options.brightness = 65;

  rgb_matrix::ParseOptionsFromFlags(&argc, &argv, &options, &runtime_opt);

  // SolarCube custom args (leftover after rgb-matrix flag parsing):
  //   --yaw=<deg>   rotate the whole scene around +Y (0/90/180/270 are most useful)
  //   --reverse     reverse orbit direction
  double yaw_deg = 0.0;
  bool reverse_orbits = false;
  for (int i = 1; i < argc; i++) {
    if (std::strncmp(argv[i], "--yaw=", 6) == 0) {
      yaw_deg = std::atof(argv[i] + 6);
    } else if (std::strcmp(argv[i], "--reverse") == 0) {
      reverse_orbits = true;
    }
  }

  RGBMatrix* matrix = RGBMatrix::CreateFromOptions(options, runtime_opt);
  if (matrix == nullptr) return 1;

  matrix->SetBrightness((uint8_t)(options.brightness ? options.brightness : 65));

  FrameCanvas* canvas = matrix->CreateFrameCanvas();
  cube::CubeCanvas cube_canvas(canvas, 64, 64);
  Canvas* draw = &cube_canvas;

  const int N = 64;

  const float yaw_rad = float(yaw_deg * 3.1415926535 / 180.0);

  std::mt19937 rng((unsigned)std::chrono::high_resolution_clock::now().time_since_epoch().count());
  std::uniform_real_distribution<float> unif(-1.0f, 1.0f);
  std::vector<Vec3> stars;
  stars.reserve(900);
  for (int i = 0; i < 900; i++) stars.push_back(vnorm(Vec3{unif(rng), unif(rng), unif(rng)}));

  // A sparse, slightly brighter near layer for subtle parallax.
  std::vector<Vec3> stars_near;
  stars_near.reserve(180);
  for (int i = 0; i < 180; i++) stars_near.push_back(vnorm(Vec3{unif(rng), unif(rng), unif(rng)}));

  Vec3 sun_dir = vnorm(Vec3{0.15f, 0.10f, 1.0f});

  std::vector<Planet> planets = {
      {"Mercury", 0.80f, 0.05f, 0.10f, 0.0f, 2.0f, 200, 200, 190, 0.55f},
      {"Venus",   0.55f, 0.03f, 0.12f, 1.2f, 3.0f, 230, 210, 120, 0.80f},
      {"Earth",   0.40f, 0.08f, 0.18f, 2.4f, 3.0f, 110, 160, 255, 1.05f},
      {"Mars",    0.32f, 0.06f, 0.22f, 3.1f, 2.0f, 230, 120,  80, 1.30f},
      {"Jupiter", 0.22f, 0.04f, 0.15f, 0.7f, 5.0f, 220, 180, 120, 1.70f},
      {"Saturn",  0.18f, 0.09f, 0.25f, 1.7f, 5.0f, 220, 200, 140, 2.05f},
  };

// Orbit trails: store recent directions per planet.
const int trail_len = 24;
std::vector<std::deque<Vec3>> trails(planets.size());



  auto t0 = std::chrono::steady_clock::now();
  const int target_ms = 33;

  while (g_running) {
    auto frame_start = std::chrono::steady_clock::now();
    float t = std::chrono::duration<float>(frame_start - t0).count();

    draw->Clear();

    DrawStarField(draw, stars, t, yaw_rad, N);
    DrawStarFieldNear(draw, stars_near, t, yaw_rad, N);

    float pulse = 0.70f + 0.30f * std::sin(t * 1.2f);
    uint8_t sr = (uint8_t)clampf(190.0f + 60.0f * pulse, 0.0f, 255.0f);
    uint8_t sg = (uint8_t)clampf(120.0f + 80.0f * pulse, 0.0f, 255.0f);
    uint8_t sb = (uint8_t)clampf( 30.0f + 20.0f * pulse, 0.0f, 255.0f);
    DrawDiscOnSurface(draw, sun_dir, 6.5f, yaw_rad, sr, sg, sb, N);

    Vec3 s = vnorm(sun_dir);
    Vec3 up = (std::fabs(s.y) < 0.9f) ? Vec3{0,1,0} : Vec3{0,0,1};
    Vec3 bx = vnorm(vcross(up, s));
    Vec3 by = vnorm(vcross(s, bx));

    for (size_t pi = 0; pi < planets.size(); pi++) {
      const auto& p = planets[pi];
      float tt = reverse_orbits ? -t : t;
      float a = tt * p.speed + p.phase;
      Vec3 offset = vadd(vmul(bx, std::cos(a) * p.distance),
                         vmul(by, std::sin(a) * p.distance));
      offset = rotate_x(offset, p.tilt_x);
      offset = rotate_z(offset, p.tilt_z);

      Vec3 dir = vnorm(vadd(s, offset));

      // Update trail buffer
      trails[pi].push_back(dir);
      while ((int)trails[pi].size() > trail_len) trails[pi].pop_front();

      // Draw trail (oldest faint, newest brighter)
      for (size_t ti = 0; ti < trails[pi].size(); ti++) {
        float f = 0.10f + 0.55f * float(ti) / float(trail_len);
        DrawDiscOnSurface(draw, trails[pi][ti], 1.0f, yaw_rad,
                          dim_u8(p.r, f), dim_u8(p.g, f), dim_u8(p.b, f), N);
      }

      DrawDiscOnSurface(draw, dir, p.radius_px, yaw_rad, p.r, p.g, p.b, N);

      if (std::strcmp(p.name, "Saturn") == 0) {
        DrawSaturnRing(draw, dir, p.radius_px, yaw_rad, N);
      }

      Vec3 trail = vnorm(vadd(s, vmul(offset, 0.92f)));
      DrawDiscOnSurface(draw, trail, 1.0f, yaw_rad,
                        (uint8_t)(p.r/4), (uint8_t)(p.g/4), (uint8_t)(p.b/4), N);
    }

    canvas = matrix->SwapOnVSync(canvas);
    cube_canvas.SetBase(canvas);

    auto frame_end = std::chrono::steady_clock::now();
    int elapsed_ms = (int)std::chrono::duration_cast<std::chrono::milliseconds>(frame_end - frame_start).count();
    int sleep_ms = target_ms - elapsed_ms;
    if (sleep_ms > 0) std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
  }

  draw->Clear();
  canvas = matrix->SwapOnVSync(canvas);
  cube_canvas.SetBase(canvas);

  delete matrix;
  return 0;
}