/* 14-segment desk display                          */
/* CC BY-NC-SA Jeroen Brinkman                      */
/* Version R2 - 2026  [blocking architecture — no struct array, no overflow]                                */
/* Universal scrolling desk display                 */
/* 14-segment version                               */

// =============================================================================
// Target  : Arduino Nano 
// Display : HT16K33 14-segment alphanumeric display
// Library : NoiascaHt16k33 (https://werner.rothschopf.net/201909_arduino_ht16k33.htm)
// Wiring  : SDA = A4  |  SCL = A5  |  VCC = 5V  |  GND = GND
//           Multiple 4-digit modules: addresses 0x70, 0x71, 0x72 ... (ascending)
// =============================================================================

// ╔═══════════════════════════════════════════════════════════════════════════╗
// ║ All user defined lines are below a ╔══╗ block like this. There are 4.     ║
// ╚═══════════════════════════════════════════════════════════════════════════╝

// ╔═══════════════════════════════════════════════════════════════════════════╗
// ║  1. Basic settings                                                        ║
// ╚═══════════════════════════════════════════════════════════════════════════╝

// ── Number of 4-digit HT16K33 modules in the chain ───────────────────────────
//   1 module  =  4 digits  (address 0x70)
//   2 modules =  8 digits (addresses 0x70, 0x71)
//   3 modules = 12 digits (addresses 0x70, 0x71, 0x72)

#define NUM_MODULES       3       // ← number of chained 4-digit modules

// ── Playlist properties ────────────────────────────────────────────────────────

#define PLAYLIST d:\Projecten\Desk displays\Software\playlist.h 
//#define PLAYLIST playlist.h       // Name of the playlist file. THIS FILE MUST BE IN THE SAME DIRECTORY AS YOUR .INO FILE
#define MAX_RANDOM_TEXTS  6       // Maximum texts per randomText() step
#define MAX_TEXT_LEN     64       // Maximum characters per text string plus one (including '\0')

// ╔═══════════════════════════════════════════════════════════════════════════╗
// ║  2. Hardware settings                                                     ║
// ╚═══════════════════════════════════════════════════════════════════════════╝

// ── I2C pins are fixed on the Nano: SDA = A4, SCL = A5 ──────────────────────
//
//   MODULE ORDER AND ADDRESSING
//   ─────────────────────────────────────────────────────────────────────────
//   View the back (solder side) of the modules placed side by side.
//   The display on the RIGHT side of the solder side has the LOWEST address.
//   That is the same display that is visible on the LEFT side at the front.
//
//   Set address with solder bridges (3 pads: A0, A1, A2):
//     No pads soldered closed            → 0x70  (factory default)
//     Only A0 soldered closed            → 0x71
//     Only A1 soldered closed            → 0x72
//     A0 and A1 soldered closed          → 0x73
//     Only A2 soldered closed            → 0x74
//   ─────────────────────────────────────────────────────────────────────────
//   Set the address of the LEFT module below (lowest address).
//   The other modules automatically receive incrementing addresses (+1, +2).
//
#define I2C_ADDR_BASE  0x70      // <- address of the LEFT module (solder side: rightmost)

// ── WDT ──────────────────────────────────────────────────────────────────────
// BOOTTYPE: 1 = AVR old bootloader (no HW WDT)
//           2 = Arduino AVR new bootloader (WDT available)
//           3 = MiniCore bootloader (WDT available)
#define BOOTTYPE  2   // <- 1 = AVR old  |  2 = AVR new  |  3 = MiniCore

// ╔═══════════════════════════════════════════════════════════════════════════╗
// ║  3. Animation timing – adjust to taste                                    ║
// ╚═══════════════════════════════════════════════════════════════════════════╝

#define LED_BRIGHT     12    // Brightness:                                0 (dim) … 15 (bright)
#define LED_DIM         0    // Dim brightness for Blink:                  0 = fully off, 1 = barely on

#define SCROLL_SPEED  150    // iScroll / oScroll / iReverse / oReverse:   ms per digit
#define PRINT_SPEED   280    // iPrint / oWipe:                             ms per column-group
#define VERT_SPEED    150    // iUncover / iDrop / iClimb / oCover / oDrop / oClimb: ms per frame
#define RAIN_SPEED    250    // iRain / oRain:                              ms per frame per digit
#define TELEX_SPEED   200    // iTelex / oTelex:                            ms per digit
#define WIPE_SPEED     50    // iWipe / oWipe:                              ms per column
#define RACE_SPEED     60    // iRace / oRace:                              ms per column per digit
#define CURTAIN_SPEED 120    // iCurtain / oCurtain:                        ms per column step
#define DISSOLVE_SPEED 50    // iDissolve / oDissolve:                      ms per segment frame
#define BLINK_SPEED   400    // Blink emphasis:                             ms per half-period
#define FADE_SPEED     80    // iFade / oFade:                              ms per brightness step
#define FADE_TAIL     300    // iFade / oFade first/last 2 dim steps:       ms
#define STAY_POLL_MS  60000  // fx_eStay(): delay interval while holding forever (ms)

// ###########################################################################
// Do not change:  Types and alignment + forward declarations
// ###########################################################################

// ── Types and alignment (needed by forward declarations and thePlaylist) ────────
typedef uint8_t EffectType;
typedef uint8_t EmphasisType;
typedef uint8_t AlignType;
#define Left     0
#define Center   1
#define Right    2

// Forward declarations for thePlaylist()
void setValue(uint16_t v);
void setText(const char *t);
void setFont(uint8_t sz);
void setAlign(AlignType a);
void setInversion(bool on);
void randomText(const char *t1, const char *t2 = nullptr, const char *t3 = nullptr,
                const char *t4 = nullptr, const char *t5 = nullptr, const char *t6 = nullptr);
void effect(EffectType en, EmphasisType em, EffectType ex, uint16_t pauseN = 0);

// ╔═══════════════════════════════════════════════════════════════════════════╗
// ║  4. User Playlist – CHANGE THIS                                           ║
// ╠═══════════════════════════════════════════════════════════════════════════╣
// ║  setValue(n)           – n × 100 ms for Wait; n cycles for Blink          ║
// ║  setText("text")       – set message text (long strings are truncated)    ║
// ║  setFont(Small|Medium|Large) – no-op; included for DotMatrix compatibility║
// ║  setAlign(Left|Center|Right) – alignment (default = Center)               ║
// ║  setInversion(true)    – invert segments                                  ║
// ║  randomText("a","b",…) – random text pool                                 ║
// ║  effect(Entrance, Emphasis, Exit)         – plays the step                ║
// ║  effect(Entrance, Emphasis, Exit, pauseN) – same + blank pause after      ║
// ║  delay(ms)             – optional pause between steps                     ║
// ║  All setter values PERSIST until explicitly changed.                      ║
// ╚═══════════════════════════════════════════════════════════════════════════╝
// ── Entrance effects ──────────────────────────────────────────────────────────
#define iNone        0   // No entrance – display stays as-is; use for chained effects.
#define iShow        1   // Full text snaps on instantly without animation.
#define iScroll      2   // Text slides in from the right edge.
#define iReverse     3   // Text slides in from the left edge.
#define iFade        4   // Text fades in: brightness steps from 0 to maximum simultaneously.
#define iPrint       5   // Each character materialises inside-out (spine first), L→R.
#define iTelex       6   // Each character snaps on all at once in sequence, L→R.
#define iDissolve    7   // Random segments light up one by one until the full text is visible.
#define iUncover     8   // All digits simultaneously: segments fill in from bottom to top.
#define iDrop        9   // All digits simultaneously: text drops in from above (physical remap).
#define iClimb      10   // All digits simultaneously: text rises in from below (physical remap).
#define iRain       11   // Text drops in digit by digit L→R (like iDrop but sequential).
#define iWipe       12   // Text appears column by column L→R; a cursor marks the next position.
#define iRace       13   // Segment-columns race in from the right, landing digit by digit L→R.
#define iCurtain    14   // Theater curtain opens: columns revealed from center outward.
#define iRandom    255   // Random entrance (excludes iNone and iShow), no-repeat.

// ── Emphasis ──────────────────────────────────────────────────────────────────
#define None         0   // No pause; entrance flows directly into exit.
#define Wait         1   // Hold full text for n × 100 ms.  Requires setValue(n).
#define Blink        2   // Blink n times at BLINK_SPEED.   Requires setValue(n).
#define Stay         3   // Freeze display forever; playlist stops.

// ── Exit effects ──────────────────────────────────────────────────────────────
#define oNone        0   // No exit – display stays as-is; use for chained effects.
#define oHide        1   // Full text snaps off instantly without animation.
#define oScroll      2   // Text slides out to the left edge.
#define oReverse     3   // Text slides out to the right edge.
#define oFade        4   // Text fades out: brightness steps from maximum to 0 simultaneously.
#define oPrint       5   // Each character dissolves outside-in (verticals first), L→R.
#define oTelex       6   // Each character snaps off all at once in sequence, R→L.
#define oDissolve    7   // Random segments extinguish one by one until the display is blank.
#define oCover       8   // All digits simultaneously: segments disappear from top to bottom.
#define oDrop        9   // All digits simultaneously: text sinks out through the bottom (physical remap).
#define oClimb      10   // All digits simultaneously: text rises out through the top (physical remap).
#define oRain       11   // Text sinks out digit by digit L→R (like oDrop but sequential).
#define oWipe       12   // Text disappears column by column R→L; a cursor marks the erased position.
#define oRace       13   // Segment-columns exit to the left, dismantling digit by digit L→R.
#define oCurtain    14   // Theater curtain closes: columns extinguished from edges inward.
#define oRandom    255   // Random exit (excludes oNone and oHide), no-repeat.

void thePlaylist() {
  // Use the include below or remove the include and put the playlist directly below this line 
  #include PLAYLIST
}

// ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
// ▓▓ DO NOT CHANGE ANYTHING BELOW THIS LINE                                                          ▓▓
// ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓

// ###########################################################################
// INTERNAL CONSTANTS – segment masks, types, includes, display object
// (No changes needed below this line)
// ###########################################################################

// ── Font size constants (compatibility with DotMatrix version) ─────────────────
#define Small    1    // setFont(Small)  – no-op on 14-segment
#define Medium   2    // setFont(Medium) – no-op on 14-segment
#define Large    3    // setFont(Large)  – no-op on 14-segment

typedef uint8_t EffectType;
typedef uint8_t EmphasisType;
typedef uint8_t AlignType;

#define Left     0
#define Center   1
#define Right    2

// ── 14-segment bit positions (Adafruit/Noiasca mapping) ──────────────────────
//   bit0=A(top bar)      bit1=B(upper-right)  bit2=C(lower-right)
//   bit3=D(bottom bar)   bit4=E(lower-left)   bit5=F(upper-left)
//   bit6=G1(left-middle) bit7=G2(right-middle)
//   bit8=H(upper-left diagonal)   bit9=J(upper-right diagonal)
//   bit10=K(lower-right diagonal) bit11=L(lower-center vertical)
//   bit12=M(lower-left diagonal)  bit13=N/I(upper-center vertical)
//   bit14=DP
//
//       AAA
//      F H N J B     EF   = left verticals    = F(5)+E(4)        = 0x0030
//      F H N J B     HG1M = left diag+mid     = H(8)+G1(6)+M(12) = 0x1140
//       G1  G2        AILD = center bars+verts = A(0)+N(13)+L(11)+D(3) = 0x2809
//      E M L K C     JG2K = right diag+mid    = J(9)+G2(7)+K(10) = 0x0680
//      E M L K C     BC   = right verticals   = B(1)+C(2)        = 0x0006
//       DDD

// ── 14-seg column group masks ─────────────────────────────────────────────────
#define SEGCOL14_EF    0x0030  // F(bit5) + E(bit4)
#define SEGCOL14_HG1M  0x1140  // H(bit8) + G1(bit6) + M(bit12)
#define SEGCOL14_AILD  0x2809  // A(bit0) + N/I(bit13) + L(bit11) + D(bit3)
#define SEGCOL14_JG2K  0x0680  // J(bit9) + G2(bit7) + K(bit10)
#define SEGCOL14_BC    0x0006  // B(bit1) + C(bit2)

// ── 14-seg row masks – CUMULATIVE (bottom→top build-up) ───────────────────────
//   R1-R4: for iUncover, iClimb, oCover, oClimb
//   D1-D4: for iDrop, iRain, oDrop, oRain (top→bottom build-up)
//
//   Row bottom→top:  D → +ECKLM → +G1G2 → +FBJHN → +A
//   R1 = D                                    = 0x0008
//   R2 = D + E(4)+C(2)+K(10)+L(11)+M(12)     = 0x1C1C
//   R3 = R2 + G1(6)+G2(7)                    = 0x1CDC
//   R4 = R3 + F(5)+B(1)+J(9)+H(8)+N/I(13)   = 0x3FFE
//   Full = R4 + A(0)                          = 0x3FFF
#define SMASK14_R1   0x0008
#define SMASK14_R2   0x1C1C
#define SMASK14_R3   0x1CDC
#define SMASK14_R4   0x3FFE

//   Row top→bottom:  A → +FBJHN → +G1G2 → +ECKLM → +D
//   D1 = A                                    = 0x0001
//   D2 = A + F(5)+B(1)+J(9)+H(8)+N/I(13)     = 0x2323
//   D3 = D2 + G1(6)+G2(7)                    = 0x23E3
//   D4 = D3 + E(4)+C(2)+K(10)+L(11)+M(12)   = 0x3FF7
//   Full = D4 + D(3)                          = 0x3FFF
#define SMASK14_D1   0x0001
#define SMASK14_D2   0x2323
#define SMASK14_D3   0x23E3
#define SMASK14_D4   0x3FF7


#define NUM_DIGITS  (NUM_MODULES * 4)
#define SEG_BUF_MAX  MAX_TEXT_LEN   // Segment buffer size – driven by MAX_TEXT_LEN


// ###########################################################################
// INCLUDES AND DISPLAY OBJECT
// ###########################################################################

#include <Wire.h>
#include "NoiascaHt16k33.h"

// Access the 14-segment font table from the Noiasca library.
extern const uint16_t charTable14[] PROGMEM;

// One display object per 4-digit module.
Noiasca_ht16k33_hw_14 _disp[NUM_MODULES];

// ###########################################################################
// ENGINE — blocking architecture
//   loop() calls thePlaylist(). thePlaylist() calls effect().
//   effect() runs entrance → emphasis → exit sequentially and then returns.
//   No struct array, no step index, no overflow possible.
// ###########################################################################

// WDT configured in hardware settings (section 2).
#if BOOTTYPE != 1
  #include <avr/wdt.h>
#endif

static void _wdtReset() {
  #if BOOTTYPE != 1
    wdt_reset();
  #endif
}

static void _safeDelay(uint32_t ms) {
  uint32_t end = millis() + ms;
  while ((int32_t)(end - millis()) > 0) _wdtReset();
}

// ###########################################################################
// HARDWARE ABSTRACTION
// ###########################################################################

static void hw_begin() {
  Wire.begin();
  for (uint8_t m = 0; m < NUM_MODULES; m++)
    _disp[m].begin(I2C_ADDR_BASE + m);
}
static void hw_setBrightness(uint8_t b) {
  for (uint8_t m = 0; m < NUM_MODULES; m++) _disp[m].setBrightness(b);
}
static void hw_setSegments(const uint16_t *segs) {
  for (uint8_t i = 0; i < NUM_DIGITS; i++)
    _disp[i / 4].writeLowLevel(i % 4, segs[i]);
}
static void hw_clear() {
  for (uint8_t m = 0; m < NUM_MODULES; m++) _disp[m].clear();
}

// ###########################################################################
// SETTER FUNCTIONS — called from thePlaylist()
// ###########################################################################

static const char  *_pendingTxt       = nullptr;
static uint16_t     _pendingN         = 0;
static AlignType    _pendingAlign     = Center;
static bool         _pendingInvert    = false;
static const char  *_pendingPool[MAX_RANDOM_TEXTS];
static uint8_t      _pendingPoolCount = 0;

void setValue(uint16_t v)   { _pendingN = v; }
void setText(const char *t) { _pendingTxt = t; _pendingPoolCount = 0; }
void setFont(uint8_t sz)    { (void)sz; }   // no-op: 14-segment has one fixed font
void setAlign(AlignType a)  { _pendingAlign = a; }
void setInversion(bool on)  { _pendingInvert = on; }

void randomText(const char *t1, const char *t2, const char *t3,
                const char *t4, const char *t5, const char *t6) {
  _pendingPoolCount = 0;
  const char *all[MAX_RANDOM_TEXTS] = {t1, t2, t3, t4, t5, t6};
  for (uint8_t i = 0; i < MAX_RANDOM_TEXTS && all[i]; i++)
    _pendingPool[_pendingPoolCount++] = all[i];
  _pendingTxt = _pendingPool[0];
}

// ###########################################################################
// CORE HELPERS
// ###########################################################################

static uint16_t charToSeg14(char c) {
  if ((uint8_t)c < 32 || (uint8_t)c > 126) return 0x0000;
  return pgm_read_word(&charTable14[(uint8_t)c - 32]);
}

// buildSegArray() — convert string to segment array.
//   Long strings are silently truncated to maxLen (no error shown).
//   Periods are OR-ed into the preceding digit as a decimal-point bit.
static uint8_t buildSegArray(const char *msg, uint16_t *buf, uint8_t maxLen) {
  uint8_t col = 0;
  for (const char *p = msg; *p && col < maxLen; p++) {
    uint8_t c = (uint8_t)*p;
    if (c >= 0x80 && c <= 0xBF) continue;   // skip UTF-8 continuation bytes
    if (c == '.') {
      if (col > 0) buf[col - 1] |= 0x4000;  // OR decimal-point into previous digit
      else         buf[col++]    = 0x4000;   // standalone DP at position 0
      continue;
    }
    buf[col++] = charToSeg14(c);
  }
  uint8_t used = col;
  while (col < maxLen) buf[col++] = 0x0000;
  return used;
}

static void applyAlign(uint16_t *segs, uint8_t len, AlignType align) {
  if (len >= NUM_DIGITS || align == Left) return;
  uint8_t offset = (align == Center) ? (NUM_DIGITS - len) / 2 : NUM_DIGITS - len;
  for (int8_t i = (int8_t)NUM_DIGITS - 1; i >= 0; i--) {
    int8_t src = i - (int8_t)offset;
    segs[i] = (src >= 0 && src < (int8_t)len) ? segs[src] : 0x0000;
  }
}

static void invertSegs(uint16_t *segs) {
  for (uint8_t i = 0; i < NUM_DIGITS; i++)
    if (segs[i]) segs[i] = (~segs[i]) & 0x3FFF;
}

static void showSegs(const uint16_t *segs) { hw_setSegments(segs); }
static void clearSegs()                    { hw_clear(); hw_setBrightness(LED_BRIGHT); }

static void maskSegs(uint16_t *dst, const uint16_t *src, uint16_t mask) {
  for (uint8_t i = 0; i < NUM_DIGITS; i++) dst[i] = src[i] & mask;
}

static int scrollLandOff(uint8_t msgLen) {
  return (msgLen > NUM_DIGITS) ? -((int)msgLen - NUM_DIGITS) : 0;
}

static void showWindow(const uint16_t *msgSegs, uint8_t msgLen, int offset) {
  uint16_t window[NUM_DIGITS];
  memset(window, 0, NUM_DIGITS * sizeof(uint16_t));
  for (uint8_t i = 0; i < NUM_DIGITS; i++) {
    int s = (int)i - offset;
    if (s >= 0 && s < (int)msgLen) window[i] = msgSegs[s];
  }
  showSegs(window);
}

static uint16_t randomBit14(uint16_t mask) {
  if (!mask) return 0;
  uint16_t bits[14]; uint8_t count = 0;
  for (uint8_t i = 0; i < 14; i++)
    if (mask & (1u << i)) bits[count++] = (1u << i);
  return bits[random(count)];
}

static void effectRange(const uint16_t *segs, uint8_t &first, uint8_t &last) {
  first = 0; last = 0; bool found = false;
  for (uint8_t i = 0; i < NUM_DIGITS; i++) {
    if (segs[i]) { if (!found) { first = i; found = true; } last = i; }
  }
}

// ###########################################################################
// PHYSICAL REMAP HELPERS  (iDrop / iClimb / oDrop / oClimb / iRain / oRain)
// ###########################################################################

static uint16_t rainEntry14F2(uint16_t s) {
  uint16_t r = 0;
  if (s & (1u <<  3)) r |= (1u <<  0);
  if (s & (1u << 11)) r |= (1u << 13);
  return r;
}
static uint16_t rainEntry14F3(uint16_t s) {
  uint16_t r = 0;
  if (s & (1u <<  3)) r |= (1u << 6) | (1u << 7);
  if (s & (1u <<  4)) r |= (1u <<  5);
  if (s & (1u <<  2)) r |= (1u <<  1);
  if (s & (1u << 10)) r |= (1u <<  9);
  if (s & (1u << 12)) r |= (1u <<  8);
  if (s & (1u << 11)) r |= (1u << 13);
  if (s & (1u <<  6)) r |= (1u <<  0);
  if (s & (1u <<  7)) r |= (1u <<  0);
  return r;
}
static uint16_t rainExit14F2(uint16_t s) {
  uint16_t r = 0;
  if (s & (1u <<  0)) r |= (1u << 6) | (1u << 7);
  if (s & (1u <<  5)) r |= (1u <<  4);
  if (s & (1u <<  1)) r |= (1u <<  2);
  if (s & (1u <<  9)) r |= (1u << 10);
  if (s & (1u <<  8)) r |= (1u << 12);
  if (s & (1u << 13)) r |= (1u << 11);
  if (s & (1u <<  6)) r |= (1u <<  3);
  if (s & (1u <<  7)) r |= (1u <<  3);
  return r;
}
static uint16_t rainExit14F3(uint16_t s) {
  uint16_t r = 0;
  if (s & (1u <<  0)) r |= (1u <<  3);
  if (s & (1u << 13)) r |= (1u << 11);
  return r;
}

// ###########################################################################
// RANDOM EFFECT RESOLUTION
// ###########################################################################

static const EffectType _randEntry[] = {
  iScroll, iReverse, iFade, iPrint, iTelex, iDissolve,
  iUncover, iCurtain, iDrop, iClimb, iRain, iWipe, iRace
};
static const EffectType _randExit[] = {
  oScroll, oReverse, oFade, oPrint, oTelex, oDissolve,
  oCover, oCurtain, oDrop, oClimb, oRain, oWipe, oRace
};
static EffectType _lastEntry = iNone;
static EffectType _lastExit  = oNone;

static EffectType _resolveRandom(EffectType fx, const EffectType *pool,
                                  uint8_t poolN, EffectType &last,
                                  uint8_t msgLen, bool isExit) {
  if ((!isExit && fx != iRandom) || (isExit && fx != oRandom)) {
    // for long text, force scroll
    if (msgLen > NUM_DIGITS) {
      if (!isExit && fx != iNone && fx != iScroll && fx != iReverse) fx = iScroll;
      if ( isExit && fx != oNone && fx != oScroll && fx != oReverse) fx = oScroll;
    }
    return fx;
  }
  uint8_t idx;
  do { idx = (uint8_t)random(poolN); } while (pool[idx] == last);
  last = pool[idx];
  return last;
}

// ###########################################################################
// ENTRANCE EFFECTS
// ###########################################################################

static void fx_iShow(const uint16_t *segs) {
  hw_setBrightness(LED_BRIGHT); showSegs(segs);
}
static void fx_iScroll(const uint16_t *segs, uint8_t msgLen) {
  hw_setBrightness(LED_BRIGHT);
  int endOff = scrollLandOff(msgLen);
  for (int off = (int)NUM_DIGITS - 1; off >= endOff; off--) {
    showWindow(segs, msgLen, off); delay(SCROLL_SPEED); _wdtReset();
  }
}
static void fx_iReverse(const uint16_t *segs, uint8_t msgLen) {
  hw_setBrightness(LED_BRIGHT);
  for (int off = -((int)msgLen - 1); off <= 0; off++) {
    showWindow(segs, msgLen, off); delay(SCROLL_SPEED); _wdtReset();
  }
}
static void fx_iFade(const uint16_t *segs) {
  hw_setBrightness(0); showSegs(segs);
  for (int b = 0; b <= LED_BRIGHT; b++) {
    hw_setBrightness(b); _safeDelay(b <= 1 ? FADE_TAIL : FADE_SPEED);
  }
}
static void fx_iPrint(const uint16_t *segs) {
  hw_setBrightness(LED_BRIGHT);
  uint16_t window[NUM_DIGITS]; memset(window, 0, NUM_DIGITS * sizeof(uint16_t));
  uint8_t first, last; effectRange(segs, first, last);
  for (uint8_t col = first; col <= last; col++) {
    if (!segs[col]) continue;
    window[col] = segs[col] & SEGCOL14_AILD;                                         showSegs(window); delay(PRINT_SPEED);
    window[col] = segs[col] & (SEGCOL14_AILD | SEGCOL14_HG1M);                       showSegs(window); delay(PRINT_SPEED);
    window[col] = segs[col] & (SEGCOL14_AILD | SEGCOL14_HG1M | SEGCOL14_JG2K);       showSegs(window); delay(PRINT_SPEED);
    window[col] = segs[col] & (SEGCOL14_AILD | SEGCOL14_HG1M | SEGCOL14_JG2K | SEGCOL14_EF); showSegs(window); delay(PRINT_SPEED);
    window[col] = segs[col];                                                           showSegs(window); delay(PRINT_SPEED);
    _wdtReset();
  }
}
static void fx_iTelex(const uint16_t *segs) {
  hw_setBrightness(LED_BRIGHT);
  uint16_t window[NUM_DIGITS]; memset(window, 0, NUM_DIGITS * sizeof(uint16_t));
  uint8_t first, last; effectRange(segs, first, last);
  for (uint8_t col = first; col <= last; col++) {
    if (!segs[col]) continue;
    window[col] = segs[col]; showSegs(window); delay(TELEX_SPEED); _wdtReset();
  }
}
static void fx_iDissolve(const uint16_t *segs) {
  hw_setBrightness(LED_BRIGHT);
  uint16_t current[NUM_DIGITS], remaining[NUM_DIGITS];
  memset(current, 0, NUM_DIGITS * sizeof(uint16_t));
  memcpy(remaining, segs, NUM_DIGITS * sizeof(uint16_t));
  for (;;) {
    uint8_t cands[NUM_DIGITS], cnt = 0;
    for (uint8_t i = 0; i < NUM_DIGITS; i++) if (remaining[i]) cands[cnt++] = i;
    if (!cnt) break;
    uint8_t idx = cands[random(cnt)];
    uint16_t bit = randomBit14(remaining[idx]);
    current[idx] |= bit; remaining[idx] &= ~bit;
    showSegs(current);
    bool anyLeft = false;
    for (uint8_t i = 0; i < NUM_DIGITS; i++) if (remaining[i]) { anyLeft = true; break; }
    if (anyLeft) { delay(DISSOLVE_SPEED); _wdtReset(); }
  }
}
static void fx_iUncover(const uint16_t *segs) {
  hw_setBrightness(LED_BRIGHT);
  uint16_t slice[NUM_DIGITS];
  maskSegs(slice, segs, SMASK14_R1); showSegs(slice); delay(VERT_SPEED);
  maskSegs(slice, segs, SMASK14_R2); showSegs(slice); delay(VERT_SPEED);
  maskSegs(slice, segs, SMASK14_R3); showSegs(slice); delay(VERT_SPEED);
  maskSegs(slice, segs, SMASK14_R4); showSegs(slice); delay(VERT_SPEED);
  showSegs(segs);
}
static void fx_iDrop(const uint16_t *segs) {
  hw_setBrightness(LED_BRIGHT);
  uint16_t f2[NUM_DIGITS], f3[NUM_DIGITS], blank[NUM_DIGITS];
  memset(blank, 0, NUM_DIGITS * sizeof(uint16_t));
  for (uint8_t i = 0; i < NUM_DIGITS; i++) { f2[i] = rainEntry14F2(segs[i]); f3[i] = rainEntry14F3(segs[i]); }
  showSegs(blank); delay(VERT_SPEED);
  showSegs(f2);    delay(VERT_SPEED);
  showSegs(f3);    delay(VERT_SPEED);
  showSegs(segs);
}
static void fx_iClimb(const uint16_t *segs) {
  hw_setBrightness(LED_BRIGHT);
  uint16_t f1[NUM_DIGITS], f2[NUM_DIGITS];
  for (uint8_t i = 0; i < NUM_DIGITS; i++) { f1[i] = rainExit14F3(segs[i]); f2[i] = rainExit14F2(segs[i]); }
  showSegs(f1); delay(VERT_SPEED);
  showSegs(f2); delay(VERT_SPEED);
  showSegs(segs);
}
static void fx_iRain(const uint16_t *segs) {
  hw_setBrightness(LED_BRIGHT);
  uint16_t window[NUM_DIGITS]; memset(window, 0, NUM_DIGITS * sizeof(uint16_t));
  uint8_t first, last; effectRange(segs, first, last);
  for (uint8_t col = first; col <= last; col++) {
    if (!segs[col]) continue;
    window[col] = 0x0000;                   showSegs(window); delay(RAIN_SPEED);
    window[col] = rainEntry14F2(segs[col]); showSegs(window); delay(RAIN_SPEED);
    window[col] = rainEntry14F3(segs[col]); showSegs(window); delay(RAIN_SPEED);
    window[col] = segs[col];                showSegs(window); delay(RAIN_SPEED);
    _wdtReset();
  }
}
static void fx_iWipe(const uint16_t *segs) {
  hw_setBrightness(LED_BRIGHT);
  uint16_t window[NUM_DIGITS]; memset(window, 0, NUM_DIGITS * sizeof(uint16_t));
  uint8_t first, last; effectRange(segs, first, last);
  for (uint8_t col = first; col <= last; col++) {
    if (!segs[col]) continue;
    window[col] = SEGCOL14_EF; showSegs(window); delay(WIPE_SPEED);
    window[col] = segs[col];   showSegs(window); delay(WIPE_SPEED);
    _wdtReset();
  }
}
static void fx_iRace(const uint16_t *segs) {
  hw_setBrightness(LED_BRIGHT);
  uint16_t window[NUM_DIGITS]; memset(window, 0, NUM_DIGITS * sizeof(uint16_t));
  static const uint16_t cols[5] = { SEGCOL14_EF, SEGCOL14_HG1M, SEGCOL14_AILD, SEGCOL14_JG2K, SEGCOL14_BC };
  for (uint8_t dst = 0; dst < NUM_DIGITS; dst++) {
    for (uint8_t ci = 0; ci < 5; ci++) {
      uint16_t segbits = segs[dst] & cols[ci]; if (!segbits) continue;
      bool isV = (cols[ci] == SEGCOL14_EF || cols[ci] == SEGCOL14_BC);
      for (int8_t pos = (int8_t)(NUM_DIGITS - 1); pos > (int8_t)dst; pos--) {
        if (isV) { window[pos] = SEGCOL14_BC; showSegs(window); delay(RACE_SPEED);
                   window[pos] = SEGCOL14_EF; showSegs(window); delay(RACE_SPEED); }
        else     { window[pos] = segbits;     showSegs(window); delay(RACE_SPEED);
                   window[pos] = segbits;     showSegs(window); delay(RACE_SPEED); }
        window[pos] = 0x0000;
      }
      window[dst] |= segbits; showSegs(window); delay(RACE_SPEED);
    }
    _wdtReset();
  }
}
static void fx_iCurtain(const uint16_t *segs) {
  hw_setBrightness(LED_BRIGHT);
  uint16_t window[NUM_DIGITS]; memset(window, 0, NUM_DIGITS * sizeof(uint16_t));
  uint8_t first, last; effectRange(segs, first, last);
  int cl = (first + last) / 2, cr = (first + last + 1) / 2;
  for (int c = cl; c <= cr; c++) window[c] = segs[c];
  showSegs(window); delay(CURTAIN_SPEED);
  for (int step = 1; step <= cl - (int)first; step++) {
    window[cl - step] = segs[cl - step];
    window[cr + step] = segs[cr + step];
    showSegs(window); delay(CURTAIN_SPEED); _wdtReset();
  }
}

// ###########################################################################
// EXIT EFFECTS
// ###########################################################################

static void fx_oHide()  { clearSegs(); }
static void fx_oFade() {
  for (int b = LED_BRIGHT; b >= 0; b--) {
    hw_setBrightness(b); _safeDelay(b <= 1 ? FADE_TAIL : FADE_SPEED);
  }
  clearSegs();
}
static void fx_oScroll(const uint16_t *segs, uint8_t msgLen) {
  int startOff = scrollLandOff(msgLen) - 1;
  for (int off = startOff; off >= -(int)msgLen; off--) {
    showWindow(segs, msgLen, off); delay(SCROLL_SPEED); _wdtReset();
  }
  clearSegs();
}
static void fx_oReverse(const uint16_t *segs, uint8_t msgLen) {
  for (int off = 1; off <= (int)NUM_DIGITS; off++) {
    showWindow(segs, msgLen, off); delay(SCROLL_SPEED); _wdtReset();
  }
  clearSegs();
}
static void fx_oWipe(const uint16_t *segs) {
  uint16_t window[NUM_DIGITS]; memcpy(window, segs, NUM_DIGITS * sizeof(uint16_t));
  for (int8_t col = (int8_t)NUM_DIGITS - 1; col >= 0; col--) {
    if (segs[col]) { window[col] = SEGCOL14_EF; showSegs(window); delay(WIPE_SPEED); }
    window[col] = 0x0000; showSegs(window); delay(WIPE_SPEED); _wdtReset();
  }
  clearSegs();
}
static void fx_oTelex(const uint16_t *segs) {
  uint16_t window[NUM_DIGITS]; memcpy(window, segs, NUM_DIGITS * sizeof(uint16_t));
  for (int8_t col = (int8_t)NUM_DIGITS - 1; col >= 0; col--) {
    if (window[col]) { window[col] = 0x0000; showSegs(window); }
    delay(TELEX_SPEED); _wdtReset();
  }
  clearSegs();
}
static void fx_oDissolve(const uint16_t *segs) {
  uint16_t current[NUM_DIGITS]; memcpy(current, segs, NUM_DIGITS * sizeof(uint16_t));
  for (;;) {
    uint8_t cands[NUM_DIGITS], cnt = 0;
    for (uint8_t i = 0; i < NUM_DIGITS; i++) if (current[i]) cands[cnt++] = i;
    if (!cnt) break;
    uint8_t idx = cands[random(cnt)];
    current[idx] &= ~randomBit14(current[idx]);
    showSegs(current);
    bool anyLeft = false;
    for (uint8_t i = 0; i < NUM_DIGITS; i++) if (current[i]) { anyLeft = true; break; }
    if (anyLeft) { delay(DISSOLVE_SPEED); _wdtReset(); }
  }
  clearSegs();
}
static void fx_oCover(const uint16_t *segs) {
  uint16_t slice[NUM_DIGITS];
  maskSegs(slice, segs, ~SMASK14_D1 & 0x3FFF); showSegs(slice); delay(VERT_SPEED);
  maskSegs(slice, segs, ~SMASK14_D2 & 0x3FFF); showSegs(slice); delay(VERT_SPEED);
  maskSegs(slice, segs, ~SMASK14_D3 & 0x3FFF); showSegs(slice); delay(VERT_SPEED);
  maskSegs(slice, segs, ~SMASK14_D4 & 0x3FFF); showSegs(slice); delay(VERT_SPEED);
  clearSegs();
}
static void fx_oClimb(const uint16_t *segs) {
  uint16_t f1[NUM_DIGITS], f2[NUM_DIGITS];
  for (uint8_t i = 0; i < NUM_DIGITS; i++) { f1[i] = rainEntry14F3(segs[i]); f2[i] = rainEntry14F2(segs[i]); }
  showSegs(f1); delay(VERT_SPEED);
  showSegs(f2); delay(VERT_SPEED);
  clearSegs();
}
static void fx_oDrop(const uint16_t *segs) {
  uint16_t f1[NUM_DIGITS], f2[NUM_DIGITS];
  for (uint8_t i = 0; i < NUM_DIGITS; i++) { f1[i] = rainExit14F2(segs[i]); f2[i] = rainExit14F3(segs[i]); }
  showSegs(f1); delay(VERT_SPEED);
  showSegs(f2); delay(VERT_SPEED);
  clearSegs();
}
static void fx_oRain(const uint16_t *segs) {
  uint16_t window[NUM_DIGITS]; memcpy(window, segs, NUM_DIGITS * sizeof(uint16_t));
  uint8_t first, last; effectRange(segs, first, last);
  for (uint8_t col = first; col <= last; col++) {
    if (!segs[col]) continue;
    window[col] = segs[col];                showSegs(window); delay(RAIN_SPEED);
    window[col] = rainExit14F2(segs[col]);  showSegs(window); delay(RAIN_SPEED);
    window[col] = rainExit14F3(segs[col]);  showSegs(window); delay(RAIN_SPEED);
    window[col] = 0x0000;                   showSegs(window); delay(RAIN_SPEED);
    _wdtReset();
  }
  clearSegs();
}
static void fx_oPrint(const uint16_t *segs) {
  uint16_t window[NUM_DIGITS]; memcpy(window, segs, NUM_DIGITS * sizeof(uint16_t));
  uint8_t first, last; effectRange(segs, first, last);
  for (uint8_t col = first; col <= last; col++) {
    if (!segs[col]) continue;
    window[col] = segs[col] & ~SEGCOL14_BC;                                               showSegs(window); delay(PRINT_SPEED);
    window[col] = segs[col] & ~(SEGCOL14_BC | SEGCOL14_EF);                              showSegs(window); delay(PRINT_SPEED);
    window[col] = segs[col] & ~(SEGCOL14_BC | SEGCOL14_EF | SEGCOL14_JG2K);              showSegs(window); delay(PRINT_SPEED);
    window[col] = segs[col] & ~(SEGCOL14_BC | SEGCOL14_EF | SEGCOL14_JG2K | SEGCOL14_HG1M); showSegs(window); delay(PRINT_SPEED);
    window[col] = 0x0000;                                                                  showSegs(window); delay(PRINT_SPEED);
    _wdtReset();
  }
  clearSegs();
}
static void fx_oRace(const uint16_t *segs) {
  uint16_t window[NUM_DIGITS]; memcpy(window, segs, NUM_DIGITS * sizeof(uint16_t));
  static const uint16_t cols[5] = { SEGCOL14_EF, SEGCOL14_HG1M, SEGCOL14_AILD, SEGCOL14_JG2K, SEGCOL14_BC };
  for (uint8_t src_d = 0; src_d < NUM_DIGITS; src_d++) {
    for (uint8_t ci = 0; ci < 5; ci++) {
      uint16_t segbits = segs[src_d] & cols[ci]; if (!segbits) continue;
      bool isV = (cols[ci] == SEGCOL14_EF || cols[ci] == SEGCOL14_BC);
      window[src_d] &= ~cols[ci];
      for (int8_t pos = (int8_t)src_d - 1; pos >= 0; pos--) {
        if (isV) { window[pos] = SEGCOL14_BC; showSegs(window); delay(RACE_SPEED);
                   window[pos] = SEGCOL14_EF; showSegs(window); delay(RACE_SPEED); }
        else     { window[pos] = segbits;     showSegs(window); delay(RACE_SPEED);
                   window[pos] = segbits;     showSegs(window); delay(RACE_SPEED); }
        window[pos] = 0x0000;
      }
      showSegs(window); delay(RACE_SPEED);
    }
    _wdtReset();
  }
  clearSegs();
}
static void fx_oCurtain(const uint16_t *segs) {
  uint16_t window[NUM_DIGITS]; memcpy(window, segs, NUM_DIGITS * sizeof(uint16_t));
  uint8_t first, last; effectRange(segs, first, last);
  int cl = (first + last) / 2, cr = (first + last + 1) / 2;
  for (int step = 0; step < cl - (int)first; step++) {
    window[(int)first + step] = 0x0000;
    window[(int)last  - step] = 0x0000;
    showSegs(window); delay(CURTAIN_SPEED); _wdtReset();
  }
  for (int c = cl; c <= cr; c++) window[c] = 0x0000;
  showSegs(window); delay(CURTAIN_SPEED);
  clearSegs();
}

// ###########################################################################
// effect() — blocking: entrance → emphasis → exit, then return
// ###########################################################################

void effect(EffectType en, EmphasisType em, EffectType ex, uint16_t pauseN) {
  // Resolve text
  const char *text = nullptr;
  if (_pendingPoolCount > 0) {
    static uint8_t _lastIdx = 255;
    uint8_t idx = (uint8_t)random(_pendingPoolCount);
    if (_pendingPoolCount > 1 && idx == _lastIdx) idx = (idx + 1) % _pendingPoolCount;
    _lastIdx = idx;
    text = _pendingPool[idx];
    _pendingPoolCount = 0;
  } else {
    text = _pendingTxt;
  }
  if (!text || !*text) { _safeDelay((uint32_t)pauseN * 100); return; }

  // Build full segment array. Scroll effects use len > NUM_DIGITS via showWindow();
  // non-scroll effects only read segs[0..NUM_DIGITS-1] and ignore extra columns.
  uint16_t segs[MAX_TEXT_LEN];
  uint8_t len = buildSegArray(text, segs, MAX_TEXT_LEN);
  applyAlign(segs, len, _pendingAlign);
  if (_pendingInvert) invertSegs(segs);

  // Resolve random effects; long text forces scroll
  uint8_t entryN = sizeof(_randEntry);
  uint8_t exitN  = sizeof(_randExit);
  EffectType entryFx = _resolveRandom(en, _randEntry, entryN, _lastEntry, len, false);
  EffectType exitFx  = _resolveRandom(ex, _randExit,  exitN,  _lastExit,  len, true);

  // ── ENTRANCE ───────────────────────────────────────────────────────────────
  switch (entryFx) {
    case iNone:    break;
    case iShow:    fx_iShow   (segs);      break;
    case iScroll:  fx_iScroll (segs, len); break;
    case iReverse: fx_iReverse(segs, len); break;
    case iFade:    fx_iFade   (segs);      break;
    case iPrint:   fx_iPrint  (segs);      break;
    case iTelex:   fx_iTelex  (segs);      break;
    case iDissolve:fx_iDissolve(segs);     break;
    case iUncover: fx_iUncover(segs);      break;
    case iDrop:    fx_iDrop   (segs);      break;
    case iClimb:   fx_iClimb  (segs);      break;
    case iRain:    fx_iRain   (segs);      break;
    case iWipe:    fx_iWipe   (segs);      break;
    case iRace:    fx_iRace   (segs);      break;
    case iCurtain: fx_iCurtain(segs);      break;
    default:       fx_iShow   (segs);      break;
  }

  // ── EMPHASIS ──────────────────────────────────────────────────────────────
  switch (em) {
    case None:  break;
    case Wait:  _safeDelay((uint32_t)_pendingN * 100); break;
    case Blink:
      for (uint16_t i = 0; i < _pendingN; i++) {
        hw_setBrightness(LED_BRIGHT); _safeDelay(BLINK_SPEED);
        hw_setBrightness(LED_DIM);    _safeDelay(BLINK_SPEED);
      }
      hw_setBrightness(LED_BRIGHT);
      break;
    case Stay:
      showSegs(segs);
      while (true) _safeDelay(60000);
      break;
  }

  // ── EXIT ──────────────────────────────────────────────────────────────────
  switch (exitFx) {
    case oNone:    break;
    case oHide:    fx_oHide   ();          break;
    case oScroll:  fx_oScroll (segs, len); break;
    case oReverse: fx_oReverse(segs, len); break;
    case oFade:    fx_oFade   ();          break;
    case oWipe:    fx_oWipe   (segs);      break;
    case oTelex:   fx_oTelex  (segs);      break;
    case oDissolve:fx_oDissolve(segs);     break;
    case oCover:   fx_oCover   (segs);     break;
    case oClimb:   fx_oClimb   (segs);     break;
    case oDrop:    fx_oDrop    (segs);     break;
    case oRain:    fx_oRain    (segs);     break;
    case oPrint:   fx_oPrint   (segs);     break;
    case oRace:    fx_oRace    (segs);     break;
    case oCurtain: fx_oCurtain (segs);     break;
    default:       fx_oHide   ();          break;
  }

  // Optional blank pause after exit
  if (pauseN > 0) { hw_clear(); _safeDelay((uint32_t)pauseN * 100); }
}

// ###########################################################################
// setup() + loop()
// ###########################################################################

void setup() {
  #if BOOTTYPE != 1
    wdt_disable();
  #endif

  uint32_t seed = (uint32_t)micros();
  for (uint8_t i = 0; i < 32; i++) {
    seed = (seed << 1) ^ (seed >> 31);
    seed ^= (uint32_t)(analogRead(A0) & 1) << 16;
    seed ^= (uint32_t)(analogRead(A1) & 1) << 8;
    seed ^= (uint32_t)(analogRead(A2) & 1);
  }
  randomSeed(seed);

  hw_begin();
  hw_setBrightness(LED_BRIGHT);
  hw_clear();

  #if BOOTTYPE != 1
    wdt_enable(WDTO_8S);
  #endif
}

void loop() {
  thePlaylist();
}
