// Use an oszilloscope as display

// $Date: 2025-12-22 14:18:49 +0100 (Mo, 22. Dez 2025) $

// for brownout detection
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"

#include "wifitime.h"

struct {
  bool minute_ticks;
  int8_t theme;  // 0=ANICH CCA, 1=tree+star, 2=snowcrystal
  String plot_uri;
} Config = {
  false, 1, "http://192.168.227.122/walls/oszi.plt"
};

// internal time format 4 digit HHMM
int getLocalTime_HHMM() {
  struct tm timeinfo;
  int t = -1;
  if (getLocalTime(&timeinfo)) {
    int h = timeinfo.tm_hour;
    t = h * 100 + timeinfo.tm_min;
  }
  return t;
}

// return month number: 1..12
int getMonth() {
  struct tm timeinfo;
  int m = -1;
  if (getLocalTime(&timeinfo)) {
    // month starts with 0
    m = timeinfo.tm_mon + 1;
  }
  return m;
}

#include "OsziGraphics.h"

OsziGraphics oszi = OsziGraphics();

const int cwid = 10;
const int cht = 10;

const int8_t JMP = 127;

const int8_t htl_lines[] = {
  // A
  30, 70,
  5, 10,
  5, -10, JMP, JMP,
  -7, 4,
  4, 0, JMP, JMP,
  // N
  -7, -18, 0, 10,
  10, -10, 0, 10,
  JMP, JMP,
  // I
  -5, -24, 0, 10,
  JMP, JMP,
  // C
  -7, -22, -2, -2,
  -5, 0, -3, 3,
  0, 4, 3, 3,
  5, 0, 2, -2, JMP, JMP,
  // C
  12, -6, -2, -2,
  -5, 0, -3, 3,
  0, 4, 3, 3,
  5, 0, 2, -2, JMP, JMP,
  // A
  2, -8,
  5, 10,
  5, -10, JMP, JMP,
  -7, 5,
  4, 0, JMP, JMP,
  // H 37+12 33
  -10, -9,
  0, -10, JMP, JMP,
  0, 5,
  -9, 0, JMP, JMP,
  0, 5,
  0, -10, JMP, JMP,
  // T
  13, 10,
  9, 0, JMP, JMP,
  -4, 0,
  0, -10, JMP, JMP,
  // L
  7, 10,
  0, -10,
  8, 0
};

const int8_t xmas_lines[] = {
  // x y
  30, 10,
  2, 10,
  -17, -2,
  -1, 0,
  17, 16,
  -9, -2,
  10, 16,
  -3, -1,
  7, 15,  // tip
  12, -19,
  -6, 1,
  13, -15,
  -8, 1,
  23, -16,
  -24, 4,
  3, -9, -12, -1,
  // no line
  JMP, JMP,
  // star
  19, 73,
  4, 10, JMP, JMP,
  1, -2,
  -13, 7,  // tip2
  14, 2,
  4, 10,  // tip 3
  3, -9, JMP, JMP,
  1, 0,
  13, -2,  // tip 4
  -13, -8,
  0, -10,        // tip 5
  -5, 8, -9, -6  // tip 1
};

const int8_t move_lines[] = {
  // snow flake?
  // x y
  40, 120,
  // 6 star
  6, 0, JMP, JMP,
  -5, -3, 5, 5, JMP, JMP,
  -3, 0, 5, -5
};

const int8_t snowman_lines[] = {
  // snow flake?
  // x y
  40, 120,
  // head
  2, 0, 2, 2, 0, 2, -2, 2, -2, 0, -2, -2, 0, -2, 2, -2,
  3, 0,  4, -4,  0, -4,  -4, -4,  -4, 0,  -4, 4,  0, 4,  4, 4, JMP, JMP,
  0, -12,
  5, 0,  6, -6,  0, -6,  -6, -6,  -6, 0,  -6, 6,  0, 6,  6, 6
};

class AnalogClock {
  bool hand_drawn = false;
public:
  // index after clock face
  int xy_index_eof = 0;

  int mid[2] = { 100, 100 };
  int watch_rad = 50;
  OsziGraphics* _oszi;

  int movement_MAX_LINE = 0;
  int8_t* movement_lines;
  char movement = 'r';  // falling snow

  AnalogClock(OsziGraphics* disp) {
    _oszi = disp;
    randomSeed(micros());
    movement_MAX_LINE = sizeof(snowman_lines) / sizeof(snowman_lines[0]);
    movement_lines = (int8_t*)&snowman_lines[0];
  }

  void drawFace() {
    _oszi->stroke(127, 127, 127);  // color
    int MAX_LINE = 0;
    int8_t* lines;
    switch (Config.theme) {
      case 1:
        MAX_LINE = sizeof(xmas_lines) / sizeof(xmas_lines[0]);
        lines = (int8_t*)&xmas_lines[0];
        // shift the watch and scale down
        mid[0] = 125;
        mid[1] = 55;
        watch_rad = 25;
        break;
      default:
        MAX_LINE = sizeof(htl_lines) / sizeof(htl_lines[0]);
        lines = (int8_t*)&htl_lines[0];
        // shift the watch and scale down
        mid[0] = 110;
        mid[1] = 50;
        watch_rad = 30;
        break;
    }

    // clock face
    for (int i = 0; i < 12; i++) {
      float _angle = PI * 2 * i / 12.;
      _oszi->line(mid[0] + watch_rad * cos(_angle),
                  mid[1] + watch_rad * sin(_angle),
                  mid[0] + (watch_rad + 7) * cos(_angle),
                  mid[1] + (watch_rad + 7) * sin(_angle));
      if (Config.minute_ticks) {
        int _sub = 10;
        for (int j = 0; j < _sub; j++) {
          float _angle = PI * 2 * (i + (j / float(_sub))) / float(_sub);
          _oszi->line(mid[0] + (watch_rad + 7) * cos(_angle),
                      mid[1] + (watch_rad + 7) * sin(_angle),
                      mid[0] + (watch_rad + 7) * cos(_angle),
                      mid[1] + (watch_rad + 7) * sin(_angle));
        }
      }
    }

    // draw optional lines
    int8_t x = lines[0];
    int8_t y = lines[1];
    for (int i = 2; i < MAX_LINE; i += 2) {
      if (lines[i] == JMP) {
      } else if (lines[i - 2] == JMP) {
        x = x + lines[i];
        y = y + lines[i + 1];
      } else {
        _oszi->line(x, y, x + lines[i], y + lines[i + 1]);
        x = x + lines[i];
        y = y + lines[i + 1];
      }
    }

    xy_index_eof = _oszi->xy_last;
  }

  int dx = 0;
  int dy = 0;
  void drawMovement() {
    if (!hand_drawn) {
      return;
    }
    // draw move lines
    _oszi->xy_last = xy_after_hands;
    switch (movement) {
      case 'f':
        // random downwards
        dx += random(0, 3) - 1;
        dy += random(0, 2) - 1;
        break;
      default:
        // random
        dx += random(0, 3) - 1;
        dy += random(0, 3) - 1;
        break;
    }
    int8_t x = movement_lines[0] + dx;
    if (x < 0) { dx = 0; }
    int8_t y = movement_lines[1] + dy;
    if (y < 0) { dy = 0; }
    for (int i = 2; i < movement_MAX_LINE; i += 2) {
      if (movement_lines[i] == JMP) {
      } else if (movement_lines[i - 2] == JMP) {
        x = x + movement_lines[i];
        y = y + movement_lines[i + 1];
      } else {
        _oszi->line(x, y, x + movement_lines[i], y + movement_lines[i + 1]);
        x = x + movement_lines[i];
        y = y + movement_lines[i + 1];
      }
    }
  }

  void drawHand(int len, float angle) {
    int cos_ = len * cos(angle);
    int sin_ = len * sin(angle);
    // BUG line is direction dependend
    _oszi->line(mid[0], mid[1], mid[0] + cos_, mid[1] + sin_);
  }

  int xy_after_hands = 0;
  void drawHands(int number) {
    // hhmm
    int mm = number % 100;
    float hh = int(number / 100) + float(mm) / 60;
    _oszi->xy_last = xy_index_eof;

    float mm_angle = PI / 2 + PI * 2 / 60. * (60 - mm);
    drawHand(watch_rad, mm_angle);

    float hh_angle = PI / 2 + PI * 2 / 12. * (12 - hh);
    drawHand(watch_rad - 10, hh_angle);
    xy_after_hands = _oszi->xy_last;
    hand_drawn = true;
  }

  void animation() {
    static int angle = 0;
    _oszi->xy_last = xy_index_eof;
    drawHand(30, angle);
    angle -= 1;
  }

  void dump() {
    _oszi->dump();
  }
};

AnalogClock aClock = AnalogClock(&oszi);

void setup() {
  Serial.begin(115200);
  // brownout detection off ... at least for wifi connect, when on PC USB
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);  //disable brownout detector

  Serial.println("ESP32 Osziclock using DAC1 and DAC2, pin 25 and 26.");
  if (!oszi.begin()) {
    Serial.println("Failed to initialize the display!");
    // impossible
  }
  // TODO face might be time/month depending. But time is after WiFi.
  Serial.print("Draw clock face:");
  aClock.drawFace();
  Serial.print(aClock.xy_index_eof);
  Serial.println("  points");

  Serial.println("\nWiFi setup");
  wifiSetup();
}

void loop() {
  static bool fetch_time = true;
  // TODO refetch_time
  if (fetch_time) {
    if (getTime_from_network()) {
      fetch_time = false;
    }
  }
  if (oszi.tick()) {
    // one page for a watch is 10ms
    static int cnt = 32000;
    if (cnt > 1000) {  // do not fetch time always
      cnt = 0;
      if (fetch_time) {
        aClock.animation();
      } else {
        static int time_ = 0;
        int t = getLocalTime_HHMM();
        if (t != time_) {
          time_ = t;
          Serial.print("localtime ");
          Serial.println(time_);
          aClock.drawHands(time_);
          //get_plot_file(Config.plot_uri);
        }
      }
    }
    if (0 == (cnt % 50)) {
      aClock.drawMovement();
    }
    cnt++;
  }
}
