// matrix_polar_cube_phase3_fixed.cc
//
// Cube-surface Matrix: spawn at TOP center (north pole), flow to TOP perimeter,
// spill onto sides, flow toward BOTTOM, converge to BOTTOM center (south pole).
//
// Physical mapping (confirmed by Jerry):
//   TOP    = face 0
//   RIGHT  = face 1
//   REAR   = face 2
//   LEFT   = face 3
//   FRONT  = face 4
//   BOTTOM = face 5
//
// TOP rotation: “top of 0 points to physical RIGHT”.
//
// Build:
//   g++ -std=c++17 -O3 -Wall -Wextra -DPHASE=3 -I../rpi-rgb-led-matrix/include \
//     matrix_polar_cube_phase3_fixed.cc -L../rpi-rgb-led-matrix/lib -lrgbmatrix -lrt -lm -lpthread \
//     -o bin/matrix_polar_cube
//
// Run:
//   sudo ./bin/matrix_polar_cube --led-rows=64 --led-cols=64 --led-chain=6 \
//     --led-gpio-mapping=adafruit-hat --led-slowdown-gpio=6 -s 60
//
// If Pi audio module is loaded, add:
//   --led-no-hardware-pulse

#ifndef PHASE
#define PHASE 3
#endif

#include <csignal>
#include <cstdint>
#include <chrono>
#include <thread>
#include <vector>
#include <random>
#include <algorithm>
#include <iostream>
#include <cmath>

#include "led-matrix.h"

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

static volatile sig_atomic_t g_running = 1;
static void SigHandler(int) { g_running = 0; }

// Faces (chain order / globe.cc)
static constexpr int F_TOP    = 0;
static constexpr int F_RIGHT  = 1;
static constexpr int F_REAR   = 2;
static constexpr int F_LEFT   = 3;
static constexpr int F_FRONT  = 4;
static constexpr int F_BOTTOM = 5;

struct G3 { int x, y, z; };

enum class AxisDir : uint8_t { Xp, Xm, Yp, Ym, Zp, Zm };
enum class LDir    : uint8_t { Up, Right, Down, Left };

// ---- Face <-> Global mapping (from your measured rotations) ----

static const char* kBuildMarker = "POLAR_CUBE_PHASE3_BOTTOMMOVE_V1";

static inline G3 FaceLocalToGlobal(int face, int lx, int ly) {
  switch (face) {
    case F_FRONT:  return { lx, 63, 63 - ly };
    case F_REAR:   return { 63 - lx, 0, 63 - ly };
    case F_RIGHT:  return { 63, ly, 63 - lx }; // local +x points -Z, local +y points +Y
    case F_LEFT:   return { 0,  ly, lx };      // local +x points +Z, local +y points +Y
    case F_TOP:    return { 63 - ly, lx, 63 }; // local +x => +Y, local +y => -X
    case F_BOTTOM: return { 63 - lx, ly, 0  }; // local +x => -X, local +y => +Y
    default:       return {0,0,0};
  }
}

static inline void GlobalToFaceLocal(int face, const G3& g, int& lx, int& ly) {
  switch (face) {
    case F_FRONT:  lx = g.x;        ly = 63 - g.z; return;
    case F_REAR:   lx = 63 - g.x;   ly = 63 - g.z; return;
    case F_RIGHT:  ly = g.y;        lx = 63 - g.z; return;
    case F_LEFT:   ly = g.y;        lx = g.z;      return;
    case F_TOP:    lx = g.y;        ly = 63 - g.x; return;
    case F_BOTTOM: lx = 63 - g.x;   ly = g.y;      return;
    default:       lx = 0;          ly = 0;        return;
  }
}

static inline int FaceFromNormal(AxisDir normal) {
  switch (normal) {
    case AxisDir::Zp: return F_TOP;
    case AxisDir::Zm: return F_BOTTOM;
    case AxisDir::Yp: return F_FRONT;
    case AxisDir::Ym: return F_REAR;
    case AxisDir::Xp: return F_RIGHT;
    case AxisDir::Xm: return F_LEFT;
  }
  return F_FRONT;
}

static inline AxisDir LocalDirToGlobalAxis(int face, LDir d) {
  switch (face) {
    case F_FRONT:
      if (d == LDir::Right) return AxisDir::Xp;
      if (d == LDir::Left)  return AxisDir::Xm;
      if (d == LDir::Down)  return AxisDir::Zm;
      return AxisDir::Zp;
    case F_REAR:
      if (d == LDir::Right) return AxisDir::Xm;
      if (d == LDir::Left)  return AxisDir::Xp;
      if (d == LDir::Down)  return AxisDir::Zm;
      return AxisDir::Zp;
    case F_RIGHT:
      if (d == LDir::Right) return AxisDir::Zm;
      if (d == LDir::Left)  return AxisDir::Zp;
      if (d == LDir::Down)  return AxisDir::Yp;
      return AxisDir::Ym;
    case F_LEFT:
      if (d == LDir::Right) return AxisDir::Zp;
      if (d == LDir::Left)  return AxisDir::Zm;
      if (d == LDir::Down)  return AxisDir::Yp;
      return AxisDir::Ym;
    case F_TOP:
      if (d == LDir::Right) return AxisDir::Yp;
      if (d == LDir::Left)  return AxisDir::Ym;
      if (d == LDir::Down)  return AxisDir::Xm;
      return AxisDir::Xp;
    case F_BOTTOM:
      if (d == LDir::Right) return AxisDir::Xm;
      if (d == LDir::Left)  return AxisDir::Xp;
      if (d == LDir::Down)  return AxisDir::Yp;
      return AxisDir::Ym;
    default:
      return AxisDir::Yp;
  }
}

static inline void StepOnCubeSurface(int& face, int& lx, int& ly, LDir step_dir) {
  int nx = lx, ny = ly;
  switch (step_dir) {
    case LDir::Up:    ny -= 1; break;
    case LDir::Down:  ny += 1; break;
    case LDir::Left:  nx -= 1; break;
    case LDir::Right: nx += 1; break;
  }

  if (nx >= 0 && nx < 64 && ny >= 0 && ny < 64) {
    lx = nx; ly = ny;
    return;
  }

  AxisDir move = LocalDirToGlobalAxis(face, step_dir);
  int new_face = FaceFromNormal(move);

  G3 g = FaceLocalToGlobal(face, lx, ly);

  switch (move) {
    case AxisDir::Xp: g.x = 63; break;
    case AxisDir::Xm: g.x = 0;  break;
    case AxisDir::Yp: g.y = 63; break;
    case AxisDir::Ym: g.y = 0;  break;
    case AxisDir::Zp: g.z = 63; break;
    case AxisDir::Zm: g.z = 0;  break;
  }

  int nlx=0, nly=0;
  GlobalToFaceLocal(new_face, g, nlx, nly);
  nlx = std::clamp(nlx, 0, 63);
  nly = std::clamp(nly, 0, 63);

  face = new_face;
  lx = nlx;
  ly = nly;
}

// ---- Drop logic ----

struct Drop {
  int face = F_TOP;
  int x = 32;
  int y = 32;
  int tx = 0;     // TOP perimeter target
  int ty = 0;
  float speed = 1.0f;   // cells per frame (accumulated)
  float acc = 0.0f;     // movement accumulator
  int life = 0;
  int life_max = 240;
};

static inline LDir StepTowardTarget(int x, int y, int tx, int ty, std::mt19937& rng) {
  int dx = tx - x;
  int dy = ty - y;
  if (dx == 0 && dy == 0) {
    std::uniform_int_distribution<int> r(0,3);
    return static_cast<LDir>(r(rng));
  }
  int ax = std::abs(dx);
  int ay = std::abs(dy);
  int sum = ax + ay;
  std::uniform_int_distribution<int> pick(1, sum);
  int r = pick(rng);
  if (r <= ax) return (dx > 0) ? LDir::Right : LDir::Left;
  return (dy > 0) ? LDir::Down : LDir::Up;
}

static inline int FaceOffsetX(int face) { return face * 64; }

int main(int argc, char** argv) {
  std::signal(SIGINT, SigHandler);
  std::signal(SIGTERM, SigHandler);
  std::cerr << "Build: " << kBuildMarker << "\n";

  RGBMatrix::Options options;
  rgb_matrix::RuntimeOptions rt;
  if (!rgb_matrix::ParseOptionsFromFlags(&argc, &argv, &options, &rt)) return 1;

  RGBMatrix* matrix = RGBMatrix::CreateFromOptions(options, rt);
  if (!matrix) {
    std::cerr << "Failed to create matrix\n";
    return 1;
  }

  matrix->SetBrightness(options.brightness ? options.brightness : 70);
  FrameCanvas* canvas = matrix->CreateFrameCanvas();

  const int W = 6 * 64;
  const int H = 64;
  std::vector<uint8_t> inten(W * H, 0);

  std::mt19937 rng((uint32_t)std::chrono::high_resolution_clock::now().time_since_epoch().count());
  std::uniform_real_distribution<float> dist01(0.0f, 1.0f);

  std::vector<Drop> drops;
  drops.reserve(600);

  // Tune knobs
  const int   max_drops        = 340;
  const float spawn_per_second = 55.0f;  // cinematic default (was 80)
  const float decay            = 0.92f;

  auto pick_perimeter_target = [&](int& tx, int& ty) {
    std::uniform_int_distribution<int> sideDist(0, 3);
    std::uniform_int_distribution<int> posDist(0, 63);
    int side = sideDist(rng);
    int p = posDist(rng);
    if (side == 0) { tx = p;  ty = 0;  }
    if (side == 1) { tx = 63; ty = p;  }
    if (side == 2) { tx = p;  ty = 63; }
    if (side == 3) { tx = 0;  ty = p;  }
  };

  auto last = std::chrono::steady_clock::now();
  float spawn_accum = 0.0f;

  while (g_running) {
    auto now = std::chrono::steady_clock::now();
    std::chrono::duration<float> dt = now - last;
    last = now;

    // Spawn at TOP center
    spawn_accum += spawn_per_second * dt.count();
    int to_spawn = (int)spawn_accum;
    spawn_accum -= to_spawn;

    for (int i = 0; i < to_spawn && (int)drops.size() < max_drops; ++i) {
      Drop d;
      d.face = F_TOP;
      // jitter spawn within the 2x2 center to avoid quadrant bias
      d.x = (dist01(rng) < 0.5f) ? 31 : 32;
      d.y = (dist01(rng) < 0.5f) ? 31 : 32;
      pick_perimeter_target(d.tx, d.ty);
      d.life = 0;
      d.life_max = 180 + (int)(dist01(rng) * 120.0f);
      d.speed = 0.45f + dist01(rng) * 0.85f;  // slower: ~0.45..1.30
      d.acc = dist01(rng);
      drops.push_back(d);
    }

    // Decay buffer (tail)
    for (auto& v : inten) v = (uint8_t)(v * decay);

    
for (size_t i = 0; i < drops.size(); ) {
      Drop& d = drops[i];
      d.life++;
      if (d.life > d.life_max) {
        drops[i] = drops.back();
        drops.pop_back();
        continue;
      }

      // Cinematic motion: per-drop speed with sub-steps
      d.acc += d.speed;
      int steps = (int)d.acc;
      d.acc -= steps;
      steps = std::clamp(steps, 0, 2);  // slower + less streaky

      bool remove = false;

      for (int s = 0; s < steps && !remove; ++s) {

#if PHASE == 1
        // TOP-only: walk toward perimeter target and kill if we leave TOP
        LDir mv = StepTowardTarget(d.x, d.y, d.tx, d.ty, rng);
        if (dist01(rng) < 0.02f) {
          std::uniform_int_distribution<int> r(0,3);
          mv = static_cast<LDir>(r(rng));
        }
        int nx=d.x, ny=d.y;
        switch (mv) {
          case LDir::Up:    ny--; break;
          case LDir::Down:  ny++; break;
          case LDir::Left:  nx--; break;
          case LDir::Right: nx++; break;
        }
        if (nx < 0 || nx > 63 || ny < 0 || ny > 63) {
          remove = true;
          break;
        }
        d.x=nx; d.y=ny;

#else
        if (d.face == F_TOP) {
          // TOP: move toward perimeter target, and allow spill using StepOnCubeSurface
          LDir mv = StepTowardTarget(d.x, d.y, d.tx, d.ty, rng);
          if (dist01(rng) < 0.02f) {
            std::uniform_int_distribution<int> r(0,3);
            mv = static_cast<LDir>(r(rng));
          }
          StepOnCubeSurface(d.face, d.x, d.y, mv);
        }
        else if (d.face == F_BOTTOM) {
#if PHASE >= 3
          // BOTTOM: converge to center without highways or X burn-in.
          int oldx = d.x;
          int oldy = d.y;

          G3 g = FaceLocalToGlobal(F_BOTTOM, d.x, d.y);

          // Nearest point in the 2x2 center sink
          const int cx = (g.x < 32) ? 31 : 32;
          const int cy = (g.y < 32) ? 31 : 32;

          int dx = cx - g.x;
          int dy = cy - g.y;

          if (std::abs(dx) + std::abs(dy) <= 1) {
            remove = true;
            break;
          }

          int ax = std::abs(dx);
          int ay = std::abs(dy);
          int sum = ax + ay;

          int r = std::uniform_int_distribution<int>(1, sum)(rng);
          if (r <= ax) g.x += (dx > 0) ? 1 : -1;
          else         g.y += (dy > 0) ? 1 : -1;

          // tiny turbulence near the sink
          if (dist01(rng) < 0.02f) {
            if (dist01(rng) < 0.5f) g.x += (dist01(rng) < 0.5f) ? -1 : 1;
            else                    g.y += (dist01(rng) < 0.5f) ? -1 : 1;
          }

          g.x = std::clamp(g.x, 0, 63);
          g.y = std::clamp(g.y, 0, 63);
          g.z = 0;

          GlobalToFaceLocal(F_BOTTOM, g, d.x, d.y);

          // absolute safety: force a simple inward step if stuck
          if (d.x == oldx && d.y == oldy) {
            const int lcx = 32, lcy = 32;
            int ldx = lcx - d.x;
            int ldy = lcy - d.y;
            if (std::abs(ldx) > std::abs(ldy)) d.x += (ldx > 0) ? 1 : -1;
            else                               d.y += (ldy > 0) ? 1 : -1;
            d.x = std::clamp(d.x, 0, 63);
            d.y = std::clamp(d.y, 0, 63);
          }
#else
          remove = true;
          break;
#endif
        }
        else {
          // SIDE faces: prefer moves that reduce global Z (toward bottom).
          // Add only *micro* turbulence (no belt drift) for a more cinematic feel.
          LDir candidates[4] = {LDir::Up, LDir::Right, LDir::Down, LDir::Left};
          G3 gcur = FaceLocalToGlobal(d.face, d.x, d.y);

          int bestScore = 1e9;
          LDir best = LDir::Down;

          for (LDir cand : candidates) {
            int tf = d.face, tx = d.x, ty = d.y;
            StepOnCubeSurface(tf, tx, ty, cand);
            G3 ng = FaceLocalToGlobal(tf, tx, ty);

            int score = ng.z * 1000;  // primary: lower z
            if (gcur.z <= 2 && tf == F_BOTTOM) score -= 500; // prefer entering bottom near boundary
            score += (std::abs(ng.x - gcur.x) + std::abs(ng.y - gcur.y)) * 5; // avoid skating

            if (score < bestScore) { bestScore = score; best = cand; }
          }

          // micro-wobble: occasionally take a lateral step IF it doesn't increase Z
          if (dist01(rng) < 0.03f) {
            LDir wobble = (dist01(rng) < 0.5f) ? LDir::Left : LDir::Right;
            int tf = d.face, tx = d.x, ty = d.y;
            StepOnCubeSurface(tf, tx, ty, wobble);
            G3 ng = FaceLocalToGlobal(tf, tx, ty);
            if (ng.z <= gcur.z) best = wobble;
          }

          StepOnCubeSurface(d.face, d.x, d.y, best);
        }
#endif
      } // steps loop

      if (remove) {
        drops[i] = drops.back();
        drops.pop_back();
        continue;
      }

      
// Draw head (with bloom)
      auto addpix = [&](int x, int y, int v) {
        if (x < 0 || x >= W || y < 0 || y >= H) return;
        int idx = y * W + x;
        inten[idx] = (uint8_t)std::min(255, (int)inten[idx] + v);
      };

      int ox = FaceOffsetX(d.face) + d.x;
      int oy = d.y;

      addpix(ox, oy, 120);  // tail seed (head is drawn in white below)
      addpix(ox-1, oy, 35);
      addpix(ox+1, oy, 35);
      addpix(ox, oy-1, 35);
      addpix(ox, oy+1, 35);
      // subtle diagonals
      addpix(ox-1, oy-1, 10);
      addpix(ox+1, oy-1, 10);
      addpix(ox-1, oy+1, 10);
      addpix(ox+1, oy+1, 10);

      ++i;
    }

    // Render
    canvas->Clear();
    for (int y = 0; y < H; ++y) {
      for (int x = 0; x < W; ++x) {
        uint8_t v = inten[y * W + x];
        if (!v) continue;
        canvas->SetPixel(x, y, 0, v, 0);
      }
    }

    
    // Draw bright "heads" in near-white, distinct from green trails.
    // This overlays the intensity buffer rendering above.
    for (const auto& d : drops) {
      int hx = FaceOffsetX(d.face) + d.x;
      int hy = d.y;
      // Slightly off-white reads well on LED panels, avoids harsh clipping.
      canvas->SetPixel(hx, hy, 235, 235, 235);
    }

canvas = matrix->SwapOnVSync(canvas);
    std::this_thread::sleep_for(std::chrono::milliseconds(16));
  }

  canvas->Clear();
  canvas = matrix->SwapOnVSync(canvas);
  delete matrix;
  return 0;
}
