// bannercube.cc - scrolling banner around the side faces, static images on top/bottom
//
// Side banner order:
//   LEFT (face 3) -> FRONT (face 4) -> RIGHT (face 1) -> REAR (face 2)
// This forms one continuous 256x64 strip.
//
// Example:
//   sudo ./bin/bannercube --led-rows=64 --led-cols=64 --led-chain=6 \
//     --led-gpio-mapping=adafruit-hat --led-slowdown-gpio=4 \
//     --text "WELCOME TO THE EXHIBIT" --image images/logo.jpg \
//     --speed 24 --brightness-scale 0.7
//
// Optional:
//   --image file.jpg            use same image for top and bottom
//   --top-image file.jpg
//   --bottom-image file.jpg
//   --text "message"
//   --speed N                  pixels per second, default 24
//   --brightness-scale X       0.05 to 1.0, default 1.0
//   --text-color R,G,B         default 255,180,40
//   --bg-color R,G,B           default 0,0,0

#include <algorithm>
#include <chrono>
#include <cmath>
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <thread>
#include <vector>

extern "C" {
#include <jpeglib.h>
}

#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; }

struct RGB {
  uint8_t r, g, b;
};

struct Image {
  int w = 0;
  int h = 0;
  std::vector<uint8_t> rgb;
  bool ok = false;
};

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

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

static bool ParseRGB(const char* s, RGB& out) {
  int r = 0, g = 0, b = 0;
  if (std::sscanf(s, "%d,%d,%d", &r, &g, &b) != 3) return false;
  r = std::max(0, std::min(255, r));
  g = std::max(0, std::min(255, g));
  b = std::max(0, std::min(255, b));
  out = {(uint8_t)r, (uint8_t)g, (uint8_t)b};
  return true;
}

static Image LoadJPEG(const char* filename) {
  Image img;
  FILE* fp = std::fopen(filename, "rb");
  if (!fp) return img;

  jpeg_decompress_struct cinfo;
  jpeg_error_mgr jerr;
  cinfo.err = jpeg_std_error(&jerr);
  jpeg_create_decompress(&cinfo);
  jpeg_stdio_src(&cinfo, fp);
  jpeg_read_header(&cinfo, TRUE);
  jpeg_start_decompress(&cinfo);

  img.w = (int)cinfo.output_width;
  img.h = (int)cinfo.output_height;
  img.rgb.resize((size_t)img.w * (size_t)img.h * 3u);

  while (cinfo.output_scanline < cinfo.output_height) {
    JSAMPROW rowptr[1];
    rowptr[0] = &img.rgb[(size_t)cinfo.output_scanline * (size_t)img.w * 3u];
    jpeg_read_scanlines(&cinfo, rowptr, 1);
  }

  jpeg_finish_decompress(&cinfo);
  jpeg_destroy_decompress(&cinfo);
  std::fclose(fp);
  img.ok = true;
  return img;
}

static inline void SetFacePixel(Canvas* draw, int face, int x, int y,
                                uint8_t r, uint8_t g, uint8_t b) {
  if (x < 0 || x >= 64 || y < 0 || y >= 64) return;
  draw->SetPixel(face * 64 + x, y, r, g, b);
}

static void DrawImageToFace(Canvas* draw, int face, const Image& img, float brightness_scale) {
  if (!img.ok || img.w <= 0 || img.h <= 0) return;

  float sx = 64.0f / float(img.w);
  float sy = 64.0f / float(img.h);
  float s = std::min(sx, sy);

  int dw = std::max(1, int(std::floor(img.w * s + 0.5f)));
  int dh = std::max(1, int(std::floor(img.h * s + 0.5f)));
  int ox = (64 - dw) / 2;
  int oy = (64 - dh) / 2;

  for (int y = 0; y < dh; y++) {
    int syi = std::min(img.h - 1, int(float(y) / s));
    for (int x = 0; x < dw; x++) {
      int sxi = std::min(img.w - 1, int(float(x) / s));
      const uint8_t* p = &img.rgb[((size_t)syi * (size_t)img.w + (size_t)sxi) * 3u];
      SetFacePixel(draw, face, ox + x, oy + y,
                   scale_u8(p[0], brightness_scale),
                   scale_u8(p[1], brightness_scale),
                   scale_u8(p[2], brightness_scale));
    }
  }
}

struct Glyph {
  char ch;
  uint8_t rows[7];
};

static const Glyph kFont5x7[] = {
  {' ', {0,0,0,0,0,0,0}},
  {'!', {0b00100,0b00100,0b00100,0b00100,0b00100,0,0b00100}},
  {'-', {0,0,0,0b01110,0,0,0}},
  {'.', {0,0,0,0,0,0b00110,0b00110}},
  {':', {0,0b00110,0b00110,0,0b00110,0b00110,0}},
  {'0', {0b01110,0b10001,0b10011,0b10101,0b11001,0b10001,0b01110}},
  {'1', {0b00100,0b01100,0b00100,0b00100,0b00100,0b00100,0b01110}},
  {'2', {0b01110,0b10001,0b00001,0b00010,0b00100,0b01000,0b11111}},
  {'3', {0b11110,0b00001,0b00001,0b01110,0b00001,0b00001,0b11110}},
  {'4', {0b00010,0b00110,0b01010,0b10010,0b11111,0b00010,0b00010}},
  {'5', {0b11111,0b10000,0b10000,0b11110,0b00001,0b00001,0b11110}},
  {'6', {0b00110,0b01000,0b10000,0b11110,0b10001,0b10001,0b01110}},
  {'7', {0b11111,0b00001,0b00010,0b00100,0b01000,0b01000,0b01000}},
  {'8', {0b01110,0b10001,0b10001,0b01110,0b10001,0b10001,0b01110}},
  {'9', {0b01110,0b10001,0b10001,0b01111,0b00001,0b00010,0b11100}},
  {'A', {0b01110,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001}},
  {'B', {0b11110,0b10001,0b10001,0b11110,0b10001,0b10001,0b11110}},
  {'C', {0b01110,0b10001,0b10000,0b10000,0b10000,0b10001,0b01110}},
  {'D', {0b11110,0b10001,0b10001,0b10001,0b10001,0b10001,0b11110}},
  {'E', {0b11111,0b10000,0b10000,0b11110,0b10000,0b10000,0b11111}},
  {'F', {0b11111,0b10000,0b10000,0b11110,0b10000,0b10000,0b10000}},
  {'G', {0b01110,0b10001,0b10000,0b10111,0b10001,0b10001,0b01110}},
  {'H', {0b10001,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001}},
  {'I', {0b01110,0b00100,0b00100,0b00100,0b00100,0b00100,0b01110}},
  {'J', {0b00001,0b00001,0b00001,0b00001,0b10001,0b10001,0b01110}},
  {'K', {0b10001,0b10010,0b10100,0b11000,0b10100,0b10010,0b10001}},
  {'L', {0b10000,0b10000,0b10000,0b10000,0b10000,0b10000,0b11111}},
  {'M', {0b10001,0b11011,0b10101,0b10101,0b10001,0b10001,0b10001}},
  {'N', {0b10001,0b10001,0b11001,0b10101,0b10011,0b10001,0b10001}},
  {'O', {0b01110,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
  {'P', {0b11110,0b10001,0b10001,0b11110,0b10000,0b10000,0b10000}},
  {'Q', {0b01110,0b10001,0b10001,0b10001,0b10101,0b10010,0b01101}},
  {'R', {0b11110,0b10001,0b10001,0b11110,0b10100,0b10010,0b10001}},
  {'S', {0b01111,0b10000,0b10000,0b01110,0b00001,0b00001,0b11110}},
  {'T', {0b11111,0b00100,0b00100,0b00100,0b00100,0b00100,0b00100}},
  {'U', {0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
  {'V', {0b10001,0b10001,0b10001,0b10001,0b10001,0b01010,0b00100}},
  {'W', {0b10001,0b10001,0b10001,0b10101,0b10101,0b10101,0b01010}},
  {'X', {0b10001,0b10001,0b01010,0b00100,0b01010,0b10001,0b10001}},
  {'Y', {0b10001,0b10001,0b01010,0b00100,0b00100,0b00100,0b00100}},
  {'Z', {0b11111,0b00001,0b00010,0b00100,0b01000,0b10000,0b11111}},
};

static const uint8_t* FindGlyph(char ch) {
  char uc = (char)std::toupper((unsigned char)ch);
  for (const auto& g : kFont5x7) if (g.ch == uc) return g.rows;
  for (const auto& g : kFont5x7) if (g.ch == ' ') return g.rows;
  return nullptr;
}

static int TextWidth5x7(const std::string& s, int scale, int spacing) {
  int w = 0;
  for (size_t i = 0; i < s.size(); i++) {
    w += 5 * scale;
    if (i + 1 < s.size()) w += spacing;
  }
  return w;
}

static void DrawGlyph5x7(Canvas* draw, int strip_x, int y0, char ch, int scale,
                         RGB color, float brightness_scale) {
  const uint8_t* rows = FindGlyph(ch);
  if (!rows) return;

  for (int y = 0; y < 7; y++) {
    for (int x = 0; x < 5; x++) {
      if (!(rows[y] & (1 << (4 - x)))) continue;
      for (int yy = 0; yy < scale; yy++) {
        for (int xx = 0; xx < scale; xx++) {
          int sx = strip_x + x * scale + xx;
          int sy = y0 + y * scale + yy;
          if (sx < 0 || sx >= 256 || sy < 0 || sy >= 64) continue;

          int face, lx;
          if (sx < 64)      { face = 3; lx = sx; }
          else if (sx < 128){ face = 4; lx = sx - 64; }
          else if (sx < 192){ face = 1; lx = sx - 128; }
          else              { face = 2; lx = sx - 192; }

          SetFacePixel(draw, face, lx, sy,
                       scale_u8(color.r, brightness_scale),
                       scale_u8(color.g, brightness_scale),
                       scale_u8(color.b, brightness_scale));
        }
      }
    }
  }
}

static void DrawScrollingText(Canvas* draw, const std::string& text, int offset_px,
                              RGB text_color, RGB bg_color, float brightness_scale) {
  for (int face : {3, 4, 1, 2}) {
    for (int y = 0; y < 64; y++) {
      for (int x = 0; x < 64; x++) {
        SetFacePixel(draw, face, x, y,
                     scale_u8(bg_color.r, brightness_scale),
                     scale_u8(bg_color.g, brightness_scale),
                     scale_u8(bg_color.b, brightness_scale));
      }
    }
  }

  const int scale = 4;
  const int spacing = 4;
  const int y0 = (64 - 7 * scale) / 2;
  const int text_w = TextWidth5x7(text, scale, spacing);
  const int total_w = text_w + 64;

  int x = 256 - (offset_px % total_w);
  for (char ch : text) {
    DrawGlyph5x7(draw, x, y0, ch, scale, text_color, brightness_scale);
    x += 5 * scale + spacing;
  }

  x = 256 - (offset_px % total_w) - total_w;
  for (char ch : text) {
    DrawGlyph5x7(draw, x, y0, ch, scale, text_color, brightness_scale);
    x += 5 * scale + spacing;
  }
}

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 = 60;
  options.show_refresh_rate = false;

  std::string text = "WELCOME TO ARTCUBE";
  std::string image_path;
  std::string top_image_path;
  std::string bottom_image_path;
  float speed = 24.0f;
  float brightness_scale = 1.0f;
  RGB text_color{255, 180, 40};
  RGB bg_color{0, 0, 0};

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

  for (int i = 1; i < argc; i++) {
    if (std::strcmp(argv[i], "--text") == 0 && i + 1 < argc) {
      text = argv[++i];
    } else if (std::strncmp(argv[i], "--text=", 7) == 0) {
      text = argv[i] + 7;
    } else if (std::strcmp(argv[i], "--image") == 0 && i + 1 < argc) {
      image_path = argv[++i];
    } else if (std::strncmp(argv[i], "--image=", 8) == 0) {
      image_path = argv[i] + 8;
    } else if (std::strcmp(argv[i], "--top-image") == 0 && i + 1 < argc) {
      top_image_path = argv[++i];
    } else if (std::strncmp(argv[i], "--top-image=", 12) == 0) {
      top_image_path = argv[i] + 12;
    } else if (std::strcmp(argv[i], "--bottom-image") == 0 && i + 1 < argc) {
      bottom_image_path = argv[++i];
    } else if (std::strncmp(argv[i], "--bottom-image=", 15) == 0) {
      bottom_image_path = argv[i] + 15;
    } else if (std::strcmp(argv[i], "--speed") == 0 && i + 1 < argc) {
      speed = std::max(1.0f, (float)std::atof(argv[++i]));
    } else if (std::strncmp(argv[i], "--speed=", 8) == 0) {
      speed = std::max(1.0f, (float)std::atof(argv[i] + 8));
    } else if (std::strcmp(argv[i], "--brightness-scale") == 0 && i + 1 < argc) {
      brightness_scale = clampf((float)std::atof(argv[++i]), 0.05f, 1.0f);
    } else if (std::strncmp(argv[i], "--brightness-scale=", 19) == 0) {
      brightness_scale = clampf((float)std::atof(argv[i] + 19), 0.05f, 1.0f);
    } else if (std::strcmp(argv[i], "--text-color") == 0 && i + 1 < argc) {
      ParseRGB(argv[++i], text_color);
    } else if (std::strncmp(argv[i], "--text-color=", 13) == 0) {
      ParseRGB(argv[i] + 13, text_color);
    } else if (std::strcmp(argv[i], "--bg-color") == 0 && i + 1 < argc) {
      ParseRGB(argv[++i], bg_color);
    } else if (std::strncmp(argv[i], "--bg-color=", 11) == 0) {
      ParseRGB(argv[i] + 11, bg_color);
    }
  }

  if (top_image_path.empty() && bottom_image_path.empty() && !image_path.empty()) {
    top_image_path = image_path;
    bottom_image_path = image_path;
  }

  Image top_img, bottom_img;
  if (!top_image_path.empty()) top_img = LoadJPEG(top_image_path.c_str());
  if (!bottom_image_path.empty()) bottom_img = LoadJPEG(bottom_image_path.c_str());

  RGBMatrix* matrix = RGBMatrix::CreateFromOptions(options, runtime_opt);
  if (matrix == nullptr) return 1;
  matrix->SetBrightness((uint8_t)(options.brightness ? options.brightness : 60));

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

  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();

    if (top_img.ok) DrawImageToFace(draw, 0, top_img, brightness_scale);
    if (bottom_img.ok) DrawImageToFace(draw, 5, bottom_img, brightness_scale);

    int offset_px = (int)std::floor(t * speed);
    DrawScrollingText(draw, text, offset_px, text_color, bg_color, brightness_scale);

    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;
}
