//Copyright <2026> <Dorian Fritze>

//Redistribution and use in source and binary forms, with or without 
// modification, are permitted provided that the following conditions are met:

//1. Redistributions of source code must retain the above copyright notice, 
// this list of conditions and the following disclaimer.

//2. Redistributions in binary form must reproduce the above copyright notice,
//  this list of conditions and the following disclaimer in the documentation 
// and/or other materials provided with the distribution.

//3. Neither the name of the copyright holder nor the names of its 
// contributors may be used to endorse or promote products derived from this 
// software without specific prior written permission.

//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

// ============================================================
//  Magic Table — Sensor Visualization v3
//  Connects to ESP32-C6 over WiFi (Telnet port 23)
//
//  SETUP:
//    1. Put magic-table-sensor-map.png in your sketch's /data/ folder
//    2. Set ESP32_IP to your ESP32-C6's IP address
//    3. Run sketch — it auto-reconnects if connection drops
//
//  NODE MAP:
//    0x10  (420,508)  sequence node 1
//    0x11  (420,611)  sequence node 2
//    0x12  (516,582)  sequence node 3
//    0x13  (783,481)  cream string lights
//    0x14  (703,767)  yellow string lights
//    0x15  (942,697)  RGB red channel
//    0x16  (403, 62)  warning node (buzzer + transmitter pulse)
//    0x17  (624,724)  RGB green channel
//    0x18  (570,693)  RGB blue channel
//    0x19  (942, 72)  bugs moving
//
//  RGB LED DOTS (9 locations — pie wedges G top, B bottom-left, R bottom-right)
//    (601,491) (942,478) (857,565) (511,640) (775,640)
//    (950,621) (578,763) (513,804) (662,864)
// ============================================================

import processing.net.*;

// ── Network ─────────────────────────────────────────────────
final String ESP32_IP    = "123.456.78.90"; // your IP address given when ESP32-C6 teacher board program runs
final int    TELNET_PORT = 23;

Client    telnet;
String    buffer             = "";
int       lastConnectAttempt = 0;
final int RECONNECT_DELAY    = 3000;

// ── Layout ───────────────────────────────────────────────────
PImage    tableImg;
final int IMG_W = 1300;
final int IMG_H = 904;
final int PANEL = 240;
final int DOT_R = 9;

// ── Colors ───────────────────────────────────────────────────
color COL_PINK_OFF     = color(255,  60, 160);
color COL_PINK_ON      = color(180,   0, 255);
color COL_SEQ_PENDING  = color(255, 140,  50);
color COL_YELLOW_OFF   = color(255, 230,  60,  80);
color COL_YELLOW_ON    = color(255, 230,  60);
color COL_CREAM_OFF    = color(255, 245, 200,  80);
color COL_CREAM_ON     = color(255, 245, 200);
color COL_GREEN_OFF    = color( 60, 200,  80, 100);
color COL_GREEN_ON     = color( 60, 255, 100);
color COL_ALARM_OFF    = color(255, 100,  30, 160);
color COL_ALARM_ON     = color(255,  50,  50);
color COL_BUGS_OFF     = color(100, 200, 255, 160);
color COL_BUGS_ON      = color( 50, 255, 255);
color COL_RESERVED     = color(160, 160, 180, 120);
color COL_BG           = color( 20,  20,  25);
color COL_PANEL        = color( 28,  28,  35);
color COL_TEXT         = color(220, 220, 230);
color COL_MUTED        = color(120, 120, 140);
color COL_CONNECTED    = color( 80, 220, 120);
color COL_DISCONNECTED = color(220,  80,  80);

// RGB wedge colors — on and off states
color COL_R_ON  = color(255,  30,  30);
color COL_G_ON  = color( 30, 255,  60);
color COL_B_ON  = color( 30, 100, 255);
color COL_R_OFF = color( 80,  10,  10);
color COL_G_OFF = color( 10,  80,  20);
color COL_B_OFF = color( 10,  30,  80);

// ── Named node positions ──────────────────────────────────────
// {x, y, address}
int[][] allNodes = {
  {420, 508, 0x10},   // 0  sequence 1
  {420, 611, 0x11},   // 1  sequence 2
  {516, 582, 0x12},   // 2  sequence 3
  {783, 481, 0x13},   // 3  cream lights
  {703, 767, 0x14},   // 4  yellow lights
  {942, 697, 0x15},   // 5  RGB red channel
  {403,  62, 0x16},   // 6  warning node
  {624, 724, 0x17},   // 7  RGB green channel
  {570, 693, 0x18},   // 8  RGB blue channel
  {942,  72, 0x19},   // 9  bugs moving
};
final int NUM_NODES = 10;

final int IDX_SEQ_0 = 0;
final int IDX_SEQ_1 = 1;
final int IDX_SEQ_2 = 2;

// ── RGB LED dot positions (9 locations) ──────────────────────
// Wedge orientation: green top, blue bottom-left, red bottom-right
int[][] rgbPts = {
  {601, 491}, {942, 478}, {857, 565},
  {511, 640}, {775, 640}, {950, 621},
  {578, 763}, {513, 804}, {662, 864}
};

// ── Yellow dot positions (0x14) ───────────────────────────────
int[][] yellowPts = {
  {483,696},{430,698},{349,702},{856,707},{942,711},{799,712},
  {500,719},{520,727},{970,733},{746,763},{608,781},{858,792},
  {933,804},{679,806},{663,828},{868,858}
};

// ── Cream dot positions (0x13) ────────────────────────────────
int[][] creamPts = {
  {902,501},{750,504},{961,571},{775,587},{844,624},
  {922,633},{763,658},{551,677},{645,735}
};

// ── Green transmitter circle ──────────────────────────────────
int[] greenCenter = {413, 544};
final int GREEN_R = 58;

// ── State ─────────────────────────────────────────────────────
boolean[] nodeOn        = new boolean[NUM_NODES];
boolean   transmitterOn = false;
boolean   lights1On     = false;
boolean   lights2On     = false;
boolean   warningOn     = false;
boolean   bugsOn        = false;
boolean   rgbRedOn      = false;   // 0x15
boolean   rgbGreenOn    = false;   // 0x17
boolean   rgbBlueOn     = false;   // 0x18
int       seqStep       = 0;
boolean   connected     = false;

String[]  logLines = new String[10];
int       logHead  = 0;
float     glowPhase = 0;


// ============================================================
//  SETUP
// ============================================================
void setup() {
  size(1540, 904);
  frameRate(30);
  smooth(4);
  for (int i = 0; i < logLines.length; i++) logLines[i] = "";

  tableImg = loadImage("magic-table-sensor-map.png");
  if (tableImg == null) {
    println("ERROR: magic-table-sensor-map.png not found in /data/ folder");
  } else {
    println("Image loaded: " + tableImg.width + "x" + tableImg.height);
  }
  connectTelnet();
}


// ============================================================
//  DRAW
// ============================================================
void draw() {
  background(COL_BG);
  glowPhase += 0.06;

  if (tableImg != null) image(tableImg, 0, 0, IMG_W, IMG_H);

  drawTransmitterCircle();
  drawCreamDots();
  drawYellowDots();
  drawRGBDots();
  drawAllNodes();
  drawPanel();

  readTelnet();
  autoReconnect();
}


// ============================================================
//  DRAW — RGB PIE WEDGE DOTS
//  Green top (270° to 30°), Blue bottom-left (30° to 150°),
//  Red bottom-right (150° to 270°)
// ============================================================
void drawRGBDots() {
  for (int[] pt : rgbPts) {
    int x = pt[0];
    int y = pt[1];

    // draw three wedges using arc()
    // arc(x, y, w, h, start, stop)  angles in radians
    // green: top wedge    270° → 390° (i.e. -90° → 30°)
    // blue:  bottom-left  30°  → 150°
    // red:   bottom-right 150° → 270°

    float a0 = radians(-90);   // top
    float a1 = radians( 30);   // bottom-right boundary
    float a2 = radians(150);   // bottom-left boundary
    float a3 = radians(270);   // back to top

    int d = DOT_R * 2;

    // green wedge — top
    fill(rgbGreenOn ? COL_G_ON : COL_G_OFF);
    noStroke();
    arc(x, y, d, d, a0, a1, PIE);

    // blue wedge — bottom left
    fill(rgbBlueOn ? COL_B_ON : COL_B_OFF);
    arc(x, y, d, d, a1, a2, PIE);

    // red wedge — bottom right
    fill(rgbRedOn ? COL_R_ON : COL_R_OFF);
    arc(x, y, d, d, a2, a3, PIE);

    // thin dividing lines between wedges
    stroke(20, 20, 25);
    strokeWeight(1);
    line(x, y, x + DOT_R * cos(a0), y + DOT_R * sin(a0));
    line(x, y, x + DOT_R * cos(a1), y + DOT_R * sin(a1));
    line(x, y, x + DOT_R * cos(a2), y + DOT_R * sin(a2));
    noStroke();

    // glow ring if any channel is on
    if (rgbRedOn || rgbGreenOn || rgbBlueOn) {
      float g = 0.5 + 0.5 * sin(glowPhase);
      // blend glow color from active channels
      float gr = rgbRedOn   ? 255 : 0;
      float gg = rgbGreenOn ? 255 : 0;
      float gb = rgbBlueOn  ? 255 : 0;
      stroke(gr, gg, gb, 60 + 60*g);
      strokeWeight(2 + 2*g);
      noFill();
      ellipse(x, y, d+2, d+2);
      noStroke();
    }
  }
}


// ============================================================
//  DRAW — NAMED NODES
// ============================================================
void drawAllNodes() {
  for (int i = 0; i < NUM_NODES; i++) {
    int x    = allNodes[i][0];
    int y    = allNodes[i][1];
    int addr = allNodes[i][2];
    boolean on = nodeOn[i];

    boolean isSeq     = (i == IDX_SEQ_0 || i == IDX_SEQ_1 || i == IDX_SEQ_2);
    boolean isWarning = (addr == 0x16);
    boolean isBugs    = (addr == 0x19);

    color dotColor;
    if (isWarning)        dotColor = on ? COL_ALARM_ON  : COL_ALARM_OFF;
    else if (isBugs)      dotColor = on ? COL_BUGS_ON   : COL_BUGS_OFF;
    else if (on)          dotColor = COL_PINK_ON;
    else                  dotColor = COL_PINK_OFF;

    if (on) {
      float g = 0.5 + 0.5 * sin(glowPhase);
      stroke(red(dotColor), green(dotColor), blue(dotColor), 80 + 80*g);
      strokeWeight(4 + 4*g);
    } else {
      noStroke();
    }

    fill(dotColor);
    ellipse(x, y, DOT_R*2, DOT_R*2);
    noStroke();

    // label
    fill(255, 255, 255, 180);
    textSize(7);
    textAlign(CENTER, CENTER);
    if (isSeq) {
      int seqIdx = (i == IDX_SEQ_0) ? 0 : (i == IDX_SEQ_1) ? 1 : 2;
      text(seqIdx+1, x, y);
    } else {
      text(hex(addr, 2), x, y - DOT_R - 4);
    }
  }
}


// ============================================================
//  DRAW — STRING LIGHTS + TRANSMITTER
// ============================================================
void drawYellowDots() {
  for (int[] pt : yellowPts) {
    color c = lights2On ? COL_YELLOW_ON : COL_YELLOW_OFF;
    if (lights2On) {
      float g = 0.5 + 0.5 * sin(glowPhase + 1.0);
      stroke(255, 230, 60, 60 + 60*g);
      strokeWeight(3 + 2*g);
    } else noStroke();
    fill(c);
    ellipse(pt[0], pt[1], DOT_R*2, DOT_R*2);
    noStroke();
  }
}

void drawCreamDots() {
  for (int[] pt : creamPts) {
    color c = lights1On ? COL_CREAM_ON : COL_CREAM_OFF;
    if (lights1On) {
      float g = 0.5 + 0.5 * sin(glowPhase + 2.0);
      stroke(255, 245, 200, 60 + 60*g);
      strokeWeight(3 + 2*g);
    } else noStroke();
    fill(c);
    ellipse(pt[0], pt[1], DOT_R*2, DOT_R*2);
    noStroke();
  }
}

void drawTransmitterCircle() {
  float g = 0.5 + 0.5 * sin(glowPhase);
  if (transmitterOn) {
    fill(COL_GREEN_ON, 40 + 30*g);
    stroke(COL_GREEN_ON);
    strokeWeight(3 + 2*g);
  } else {
    fill(COL_GREEN_OFF);
    stroke(60, 200, 80, 160);
    strokeWeight(2);
  }
  ellipse(greenCenter[0], greenCenter[1], GREEN_R*2, GREEN_R*2);
  noStroke();
}


// ============================================================
//  DRAW — PANEL
// ============================================================
void drawPanel() {
  int px = IMG_W;
  fill(COL_PANEL); noStroke();
  rect(px, 0, PANEL, height);

  int cy = 24;

  // connection status
  fill(connected ? COL_CONNECTED : COL_DISCONNECTED);
  ellipse(px+16, cy, 10, 10);
  fill(COL_TEXT); textSize(13); textAlign(LEFT, CENTER);
  text(connected ? "Connected" : "Reconnecting...", px+28, cy);
  fill(COL_MUTED); textSize(10);
  text(ESP32_IP + ":" + TELNET_PORT, px+28, cy+14);
  divider(px, cy+26);

  // sequence
  cy = 80;
  fill(COL_MUTED); textSize(11); textAlign(LEFT);
  text("SEQUENCE", px+14, cy); cy += 18;
  for (int i = 0; i < 3; i++) {
    boolean done   = seqStep > i;
    boolean active = seqStep == i;
    color c = done ? COL_PINK_ON : active ? COL_SEQ_PENDING : color(60,60,80);
    fill(c); rect(px+14 + i*68, cy, 56, 14, 4);
    fill(done||active ? color(255) : COL_MUTED);
    textSize(9); textAlign(CENTER, CENTER);
    text("0x1"+i, px+14 + i*68 + 28, cy+7);
  }

  // transmitter
  cy += 30;
  fill(transmitterOn ? COL_GREEN_ON : color(50,50,65));
  rect(px+14, cy, PANEL-28, 22, 5);
  fill(transmitterOn ? color(20,20,20) : COL_MUTED);
  textSize(11); textAlign(CENTER, CENTER);
  text(transmitterOn ? "TRANSMITTER ON" : "transmitter off",
       px+14+(PANEL-28)/2, cy+11);

  // warning
  cy += 28;
  fill(warningOn ? COL_ALARM_ON : color(50,50,65));
  rect(px+14, cy, PANEL-28, 22, 5);
  fill(warningOn ? color(255) : COL_MUTED);
  textSize(11); textAlign(CENTER, CENTER);
  text(warningOn ? "WARNING ACTIVE" : "warning off",
       px+14+(PANEL-28)/2, cy+11);

  // string lights
  cy += 30;
  int hw = (PANEL-32)/2;
  fill(lights1On ? COL_CREAM_ON : color(50,50,65));
  rect(px+14, cy, hw, 22, 5);
  fill(lights1On ? color(60,40,0) : COL_MUTED);
  textSize(10); textAlign(CENTER, CENTER);
  text("lights 1", px+14+hw/2, cy+11);
  fill(lights2On ? COL_YELLOW_ON : color(50,50,65));
  rect(px+14+hw+4, cy, hw, 22, 5);
  fill(lights2On ? color(60,40,0) : COL_MUTED);
  text("lights 2", px+14+hw+4+hw/2, cy+11);

  // bugs
  cy += 30;
  fill(bugsOn ? COL_BUGS_ON : color(50,50,65));
  rect(px+14, cy, PANEL-28, 22, 5);
  fill(bugsOn ? color(20,20,20) : COL_MUTED);
  textSize(11); textAlign(CENTER, CENTER);
  text(bugsOn ? "BUGS MOVING" : "bugs off",
       px+14+(PANEL-28)/2, cy+11);

  // RGB channels
  cy += 30;
  fill(COL_MUTED); textSize(10); textAlign(LEFT);
  text("RGB LEDS", px+14, cy); cy += 16;
  int tw = (PANEL-32)/3;
  // red
  fill(rgbRedOn ? COL_R_ON : color(50,20,20));
  rect(px+14, cy, tw, 20, 4);
  fill(rgbRedOn ? color(255) : COL_MUTED);
  textSize(9); textAlign(CENTER, CENTER);
  text("R 0x15", px+14+tw/2, cy+10);
  // green
  fill(rgbGreenOn ? COL_G_ON : color(20,50,20));
  rect(px+14+tw+2, cy, tw, 20, 4);
  fill(rgbGreenOn ? color(20,20,20) : COL_MUTED);
  text("G 0x17", px+14+tw+2+tw/2, cy+10);
  // blue
  fill(rgbBlueOn ? COL_B_ON : color(20,20,50));
  rect(px+14+tw*2+4, cy, tw, 20, 4);
  fill(rgbBlueOn ? color(255) : COL_MUTED);
  text("B 0x18", px+14+tw*2+4+tw/2, cy+10);

  divider(px, cy+26);

  // log
  cy += 34;
  fill(COL_MUTED); textSize(10); textAlign(LEFT);
  text("LOG", px+14, cy); cy += 14;
  textSize(9);
  for (int i = 0; i < logLines.length; i++) {
    int idx = (logHead + i) % logLines.length;
    float alpha = map(i, 0, logLines.length-1, 60, 220);
    fill(COL_TEXT, alpha);
    text(logLines[idx], px+10, cy + i*13, PANEL-14, 12);
  }

  // legend
  cy = height - 140;
  divider(px, cy); cy += 10;
  fill(COL_MUTED); textSize(10); textAlign(LEFT);
  text("LEGEND", px+14, cy); cy += 14;
  legendDot(px+14, cy, COL_PINK_OFF,  "Hall sensor");   cy += 13;
  legendDot(px+14, cy, COL_PINK_ON,   "Triggered");     cy += 13;
  legendDot(px+14, cy, COL_ALARM_OFF, "Warning node");  cy += 13;
  legendDot(px+14, cy, COL_BUGS_OFF,  "Bugs node");     cy += 13;
  legendDot(px+14, cy, COL_G_ON,      "RGB green");     cy += 13;
  legendDot(px+14, cy, COL_B_ON,      "RGB blue");      cy += 13;
  legendDot(px+14, cy, COL_R_ON,      "RGB red");
}

void divider(int px, int cy) {
  stroke(60, 60, 80); strokeWeight(1);
  line(px+10, cy, px+PANEL-10, cy);
  noStroke();
}

void legendDot(int x, int y, color c, String label) {
  fill(c); noStroke();
  ellipse(x+5, y+4, 10, 10);
  fill(COL_TEXT); textSize(9); textAlign(LEFT, TOP);
  text(label, x+18, y);
}


// ============================================================
//  NETWORK
// ============================================================
void connectTelnet() {
  try {
    telnet = new Client(this, ESP32_IP, TELNET_PORT);
    connected = true;
    addLog("Connected to " + ESP32_IP);
  } catch (Exception e) {
    connected = false;
    addLog("Connection failed");
  }
  lastConnectAttempt = millis();
}

void autoReconnect() {
  if (!connected && millis() - lastConnectAttempt > RECONNECT_DELAY) {
    connectTelnet();
  }
  if (connected && (telnet == null || !telnet.active())) {
    connected = false;
    addLog("Disconnected");
  }
}

void readTelnet() {
  if (!connected || telnet == null) return;
  if (telnet.available() > 0) {
    buffer += telnet.readString();
    String[] lines = buffer.split("\n");
    for (int i = 0; i < lines.length - 1; i++) parseLine(lines[i].trim());
    buffer = lines[lines.length - 1];
  }
}

void parseLine(String line) {
  if (line.length() == 0) return;
  addLog(line);
  println("ESP: " + line);

  // sequence
  if (line.contains("Sequence started")) seqStep = 1;
  if (line.contains("Step") && line.contains("confirmed")) {
    try {
      int si = line.indexOf("Step") + 5;
      seqStep = int(trim(line.substring(si, si+2)));
    } catch (Exception e) {}
  }
  if (line.contains("SEQUENCE COMPLETE") || line.contains("transmitter ON")) {
    seqStep = 3; transmitterOn = true;
    nodeOn[IDX_SEQ_0] = true; nodeOn[IDX_SEQ_1] = true; nodeOn[IDX_SEQ_2] = true;
  }
  if (line.contains("transmitter extended")) transmitterOn = true;
  if (line.contains("REVERSE COMPLETE") || line.contains("transmitter OFF")
      || line.contains("Losing power")) {
    transmitterOn = false; seqStep = 0;
    nodeOn[IDX_SEQ_0] = false; nodeOn[IDX_SEQ_1] = false; nodeOn[IDX_SEQ_2] = false;
  }

  // warning
  if (line.contains("Warning started"))    warningOn = true;
  if (line.contains("Losing power") || line.contains("REVERSE COMPLETE")
      || line.contains("transmitter extended")) warningOn = false;

  // string lights
  if (line.contains("0x13") && line.contains("ON"))  { lights1On = true;  nodeOn[3] = true;  }
  if (line.contains("0x13") && line.contains("OFF")) { lights1On = false; nodeOn[3] = false; }
  if (line.contains("0x14") && line.contains("ON"))  { lights2On = true;  nodeOn[4] = true;  }
  if (line.contains("0x14") && line.contains("OFF")) { lights2On = false; nodeOn[4] = false; }

  // RGB channels
  if (line.contains("0x15") && line.contains("ON"))  { rgbRedOn = true;   nodeOn[5] = true;  }
  if (line.contains("0x15") && line.contains("OFF")) { rgbRedOn = false;  nodeOn[5] = false; }
  if (line.contains("0x17") && line.contains("ON"))  { rgbGreenOn = true; nodeOn[7] = true;  }
  if (line.contains("0x17") && line.contains("OFF")) { rgbGreenOn = false;nodeOn[7] = false; }
  if (line.contains("0x18") && line.contains("ON"))  { rgbBlueOn = true;  nodeOn[8] = true;  }
  if (line.contains("0x18") && line.contains("OFF")) { rgbBlueOn = false; nodeOn[8] = false; }

  // bugs
  if (line.contains("0x19") && line.contains("ON"))  { bugsOn = true;  nodeOn[9] = true;  }
  if (line.contains("0x19") && line.contains("OFF")) { bugsOn = false; nodeOn[9] = false; }
}


// ============================================================
//  HELPERS
// ============================================================
void addLog(String msg) {
  if (msg.length() > 28) msg = msg.substring(0, 27) + "…";
  logLines[logHead % logLines.length] = msg;
  logHead++;
}

void keyPressed() {
  if (key == 'r' || key == 'R') {
    for (int i = 0; i < nodeOn.length; i++) nodeOn[i] = false;
    transmitterOn = false; lights1On = false; lights2On = false;
    warningOn = false; bugsOn = false; seqStep = 0;
    rgbRedOn = false; rgbGreenOn = false; rgbBlueOn = false;
    addLog("Manual reset");
  }
}
