/*
  Lightbulb Toggle — Processing sketch
  By gllbhh for Fab Academy

  Draws a lightbulb and a toggle switch.
  Clicking either (or clicking the bulb itself) toggles the LED on the board.
  Pressing the physical button on D7 also toggles the LED and updates the GUI.
  State is kept in sync in both directions over serial.

  How state sync works
  --------------------
  GUI click  → ledOn flipped immediately (optimistic update) → "0,<state>\n" sent
  Board btn  → Arduino sends "0,<state>\n" → ledOn set from the message

  Neither side echoes back after receiving, so state can never loop.
  The GUI is intentionally disabled (clicks ignored, toggle greyed out) until
  a serial port is opened — the board must be connected before toggling.

  Serial protocol  PC → Arduino :  "0,<state>\n"   0 = off, 1 = on
  Serial protocol  Arduino → PC :  "0,<state>\n"   0 = off, 1 = on

  No extra libraries needed — everything is drawn with Processing primitives.
*/

import processing.serial.*;   // built-in Processing serial library

// serial is null until the user opens a port.
// Keeping it as null is a simple way to check "is the board connected?"
Serial  serial;
final int BAUD = 9600;        // must match Serial.begin() in the Arduino sketch

// The single source of truth for LED state on this side.
// Updated either by a GUI click or by a message from the board.
boolean ledOn = false;

// ---- port panel state ----
String[] portList     = {};   // filled by Serial.list() when Refresh is clicked
int      selectedPort = -1;   // index into portList; -1 means nothing selected

// ---- layout constants ----
// All positions are in pixels. Using constants makes it easy to move things.

// Lightbulb: centre x/y and radius of the glass globe
final int BX = 300, BY = 205, BR = 75;

// Toggle switch: centre x/y, total width, height
// The corner radius is set to h/2 in drawToggle() to make a pill shape.
final int TX = 300, TY = 390, TW = 120, TH = 50;

// Port panel: top edge y and row height (buttons + selector sit in this strip)
final int PY = 10, PH = 34;

// =============================================================================
// SETUP & DRAW
// =============================================================================

void setup() {
  size(600, 480);
  // createFont() embeds a system font at the given size.
  // The third argument (true) enables anti-aliasing.
  textFont(createFont("Arial", 14, true));

  // Populate the port list once at startup so ports are visible immediately.
  portList = Serial.list();
}

void draw() {
  // draw() runs continuously (~60 fps by default).
  // Redrawing the whole background every frame is the standard Processing
  // pattern — it erases the previous frame before drawing the new one.
  background(28, 28, 35);

  pollSerial();        // check for incoming messages first so state is fresh
  drawPortPanel();     // Refresh / Open / Close buttons + port selector
  if (ledOn) drawGlow(BX, BY, BR);   // soft ambient glow behind the bulb
  drawBulb(BX, BY, BR, ledOn);       // the lightbulb graphic
  drawToggle(TX, TY, TW, TH, ledOn); // the pill-shaped on/off switch
  drawHint();          // instruction text at the bottom of the window
}

// =============================================================================
// SERIAL
// =============================================================================

void pollSerial() {
  // Guard: if no port is open there is nothing to read.
  if (serial == null) return;

  // Loop in case multiple lines arrived since the last frame.
  // readStringUntil() returns null if '\n' has not been received yet,
  // so the while condition naturally breaks when the buffer is empty.
  while (serial.available() > 0) {
    String line = serial.readStringUntil('\n');
    if (line == null) break;

    line = line.trim();              // strip '\n', '\r', and leading spaces
    String[] p = line.split(",");    // split "0,1" into ["0", "1"]

    // We expect at least two tokens and the key to be "0" (LED channel).
    // Any other message (malformed or for a different key) is silently ignored.
    if (p.length >= 2 && p[0].equals("0")) {
      ledOn = p[1].equals("1");      // "1" = on, anything else = off
    }
  }
}

void sendState() {
  // Sends the current ledOn value to the Arduino.
  // Called immediately after a GUI click so the board catches up
  // with the optimistic UI update.
  if (serial != null) {
    serial.write("0," + (ledOn ? "1" : "0") + "\n");
  }
}

// =============================================================================
// PORT PANEL
// =============================================================================

void drawPortPanel() {
  // Three action buttons on the left, a port selector box on the right.
  drawBtn(10,  PY, 90, PH, "Refresh");  // repopulate portList
  drawBtn(110, PY, 70, PH, "Open");     // open the selected port
  drawBtn(190, PY, 70, PH, "Close");    // close the current port

  // Port selector — a simple box that shows the currently selected port name.
  // Clicking it cycles through available ports (see mousePressed).
  fill(45, 45, 52);
  stroke(90);
  strokeWeight(1);
  rect(275, PY, 315, PH, 4);
  noStroke();
  textAlign(LEFT, CENTER);
  textSize(12);
  if (selectedPort >= 0 && selectedPort < portList.length) {
    // Green text when the port is open, white when selected but not yet open.
    fill(serial != null ? color(100, 220, 130) : color(210));
    text((serial != null ? "[open] " : "") + portList[selectedPort], 283, PY + PH / 2);
  } else if (portList.length == 0) {
    fill(110);
    text("No ports found — click Refresh", 283, PY + PH / 2);
  } else {
    fill(110);
    text("Click to select a port  (" + portList.length + " found)", 283, PY + PH / 2);
  }
}

void drawBtn(int x, int y, int w, int h, String label) {
  // A minimal flat button — dark fill, light border, centred label.
  fill(65, 65, 78);
  stroke(110);
  strokeWeight(1);
  rect(x, y, w, h, 5);   // 5 px corner radius
  fill(210);
  noStroke();
  textAlign(CENTER, CENTER);
  textSize(12);
  text(label, x + w / 2, y + h / 2);
}

// =============================================================================
// GLOW
// =============================================================================

void drawGlow(int cx, int cy, int r) {
  // Simulates a warm light halo by drawing 7 concentric ellipses at
  // decreasing opacity as they get further from the bulb centre.
  // i goes from 7 (outermost, most transparent) down to 1 (innermost, densest).
  noStroke();
  for (int i = 7; i >= 1; i--) {
    // Alpha ramps from 7 (faint outer ring) to 49 (denser inner ring).
    fill(255, 200, 70, (8 - i) * 7);
    float gr = r + i * 20;           // each ring is 20 px wider than the last
    ellipse(cx, cy, gr * 2, gr * 2);
  }
  // This is drawn before the bulb so the glow sits behind it.
}

// =============================================================================
// LIGHTBULB
// =============================================================================

void drawBulb(int cx, int cy, int r, boolean on) {

  // --- glass globe ---
  // A simple circle. Warm yellow when on, dark grey when off.
  strokeWeight(2);
  if (on) { fill(255, 235, 100); stroke(240, 200, 60); }
  else    { fill(72, 72, 80);    stroke(105); }
  ellipse(cx, cy, r * 2, r * 2);

  // --- filament ---
  // A W-shaped polyline drawn inside the globe to suggest the heating wire.
  // beginShape() / endShape() without CLOSE draws an open path (not filled).
  noFill();
  strokeWeight(on ? 2.5 : 1.5);
  if (on) stroke(255, 248, 190, 220);   // bright and slightly transparent when on
  else    stroke(135, 135, 145);         // muted grey when off
  int fw = r / 4;   // half-width of the W
  int fh = r / 2;   // full height of the W
  beginShape();
    vertex(cx - fw, cy + fh / 2);   // bottom-left
    vertex(cx - fw, cy - fh / 2);   // top-left
    vertex(cx,      cy);             // centre dip
    vertex(cx + fw, cy - fh / 2);   // top-right
    vertex(cx + fw, cy + fh / 2);   // bottom-right
  endShape();   // no CLOSE — leaves the shape open (no line back to start)

  // --- base (three stepped screw bands) ---
  // Each band is narrower than the one above it, tapering toward the tip.
  // bTop is where the base starts — just inside the bottom of the globe.
  float bTop = cy + r - 4;
  float[] bw = { r * 0.85, r * 0.65, r * 0.46 };   // width of each band
  color capFill   = on ? color(195, 175, 65)  : color(85, 85, 92);
  color capStroke = on ? color(145, 125, 45)  : color(115);
  fill(capFill);
  stroke(capStroke);
  strokeWeight(1);
  for (int i = 0; i < 3; i++) {
    // rect(x, y, w, h, radius) — x/y is top-left corner, so we offset by bw/2
    rect(cx - bw[i] / 2, bTop + i * 14, bw[i], 14, 2);
  }
}

// =============================================================================
// TOGGLE SWITCH
// =============================================================================

void drawToggle(int cx, int cy, int w, int h, boolean on) {
  // x/y are the top-left corner of the toggle track rectangle.
  int x  = cx - w / 2;
  int y  = cy - h / 2;
  // Setting the corner radius to h/2 makes the ends fully rounded — a pill.
  int cr = h / 2;

  // Check connection once and use it for all colour decisions below.
  boolean connected = serial != null;

  // --- track ---
  // The track changes colour to show state:
  //   disconnected → near-black (inactive, not clickable)
  //   connected + off → dark grey
  //   connected + on  → green
  noStroke();
  if (!connected)    fill(50, 50, 55);
  else if (on)       fill(65, 170, 90);
  else               fill(72, 72, 88);
  rect(x, y, w, h, cr);   // pill-shaped track

  // --- thumb ---
  // The thumb slides to the right when on and to the left when off.
  // Its centre is inset by one radius (cr) from each end of the track
  // so it stays fully inside the pill at both extremes.
  int thumbCx = on ? cx + w / 2 - cr : cx - w / 2 + cr;
  fill(connected ? color(230) : color(100, 100, 108));   // dim when disconnected
  ellipse(thumbCx, cy, h - 10, h - 10);

  // --- label below the toggle ---
  textAlign(CENTER, TOP);
  textSize(14);
  if (!connected)    fill(70);               // barely visible when disconnected
  else if (on)       fill(100, 225, 130);    // green when on
  else               fill(130, 130, 145);    // grey when off
  noStroke();
  text(on ? "ON" : "OFF", cx, cy + h / 2 + 8);
}

// =============================================================================
// HINT TEXT
// =============================================================================

void drawHint() {
  // A single line of instructions at the very bottom of the window.
  // The message changes depending on whether the port is open.
  textAlign(CENTER, BOTTOM);
  textSize(12);
  fill(85);
  noStroke();
  if (serial == null) {
    text("Connect to a port first, then click the bulb or toggle", width / 2, height - 8);
  } else {
    text("Click the bulb or toggle — or press the physical button on D7", width / 2, height - 8);
  }
}

// =============================================================================
// MOUSE INPUT
// =============================================================================

void mousePressed() {
  // Check buttons in the port panel first, then the interactive widgets.
  // Each branch returns immediately so only one action fires per click.

  // --- Refresh ---
  // Re-queries the OS for available serial ports and resets the selection
  // so the user can pick from the updated list.
  if (over(10, PY, 90, PH)) {
    portList = Serial.list();
    selectedPort = -1;
    return;
  }

  // --- Open ---
  // Attempts to open the selected port at the configured baud rate.
  // Wrapped in try/catch because the port might be in use by another app.
  if (over(110, PY, 70, PH)) {
    if (selectedPort >= 0 && selectedPort < portList.length) {
      try {
        serial = new Serial(this, portList[selectedPort], BAUD);
        println("Opened: " + portList[selectedPort]);
      } catch (RuntimeException e) {
        println("Could not open port: " + e.getMessage());
      }
    }
    return;
  }

  // --- Close ---
  // Stops the serial port and sets the reference to null.
  // Setting serial = null is the flag used everywhere else to mean "disconnected".
  if (over(190, PY, 70, PH)) {
    if (serial != null) { serial.stop(); serial = null; println("Port closed."); }
    return;
  }

  // --- Port selector ---
  // Clicking the box cycles forward through available ports.
  // The modulo wraps back to index 0 after the last port.
  if (over(275, PY, 315, PH)) {
    if (portList.length > 0) selectedPort = (selectedPort + 1) % portList.length;
    return;
  }

  // --- Toggle switch and bulb ---
  // Both controls are disabled (no response) when disconnected.
  // This prevents the GUI and board from getting out of sync.
  if (serial == null) return;

  // Toggle switch hit-test: rectangle centred on (TX, TY)
  if (over(TX - TW / 2, TY - TH / 2, TW, TH)) {
    ledOn = !ledOn;    // optimistic update — GUI changes instantly
    sendState();       // then inform the board
    return;
  }

  // Bulb hit-test: circular region using dist() for a natural feel
  if (dist(mouseX, mouseY, BX, BY) < BR) {
    ledOn = !ledOn;
    sendState();
    return;
  }
}

// Helper: returns true when the mouse cursor is inside a rectangle.
// Used instead of Processing's mouseX/mouseY comparisons inline to keep
// mousePressed() readable.
boolean over(int x, int y, int w, int h) {
  return mouseX >= x && mouseX <= x + w && mouseY >= y && mouseY <= y + h;
}
