/*
File:    swCharacter.js
Date:    2026-03-19
Author:  klp
Workspace: SWCharacterSaga2026-03-19-Stg1
Purpose: SWCharacter class — a single typographic character with rich,
         independently-togglable animated attributes.

=== Design Overview ===
  A SWCharacter represents ONE alphanumeric or punctuation symbol drawn on a
  SWGrid.  Its position is managed by a SWBug instance (which can wander via
  random walk or Perlin noise; Stage 1 keeps it stationary).

  Configurable display attributes
  ─────────────────────────────────────────────────────────────────────────────
  symbol       single character string (e.g. 'A', '@', '7')
  fontFamily   CSS font-family string  (e.g. 'Georgia, serif')
  baseSize     base font size in grid USER UNITS (not screen pixels)
               → converted to screen pixels at draw time via grid.xScale
  fillColor    SWColor — character fill;  null = noFill()
  strokeColor  SWColor — character border; null = noStroke()
  strokeWt     stroke weight in screen pixels

  Animated features  (each independently toggled with a boolean flag)
  ─────────────────────────────────────────────────────────────────────────────
  Breathe      size oscillates using a SWSinusoid
                 sizeUser = breatheAmount * sin(breatheSpeed * 2π * _etime) + baseSize
               breatheAmount   amplitude in user units
               breatheSpeed    cycles per second

  Spin         rotates around its centroid
               spinSpeed       radians per second

  CycleHue     fill hue advances every update(), cycling the color wheel
               hueCycleSpeed   hue degrees per second

=== Coordinate System ===
  Position (this.bug.x / this.bug.y) is in SWGrid USER coordinates.
  draw(grid) calls grid.userToScreen() to convert to canvas pixels.
  baseSize is also in user units; it is multiplied by grid.xScale (pixels per
  user unit) before being passed to p5.js textSize().

=== Usage ===
  // Inside p5.js setup() — after colorMode(HSB, ...) and initializeSWColors():
  let bug = new SWBug(100, 200, 'r', 0);   // stationary bug at (100,200) user coords
  let ch  = new SWCharacter('A', bug, {
      fontFamily:    'Georgia, serif',
      baseSize:      18,
      fillColor:     swOrange,
      strokeColor:   swBrown,
      shouldBreathe: true,
      breatheAmount: 5,
      breatheSpeed:  1,
      shouldSpin:    false,
      shouldCycleHue: false
  });

  // Inside p5.js draw():
  ch.update(1 / frameRate());   // advance animations
  ch.draw(grid);                // render onto grid

=== Dependencies ===
  p5.js      (text, fill, stroke, noFill, noStroke, push, pop, translate, rotate, textFont, textSize, textAlign)
  SWColor    (shapeClasses/swColor.js)
  SWBug      (shapeClasses/swBug.js)
  SWSinusoid (shapeClasses/swSinusoid.js)  — used conceptually; math is inline
*/

console.log("[swCharacter.js] SWCharacter class loaded.");

class SWCharacter {

    /**
     * @param {string}  symbol   - Single character to display (first char is used)
     * @param {SWBug}   bug      - Position manager in grid user coordinates
     * @param {Object}  [opts]   - Optional configuration (all optional)
     *
     * @param {string}  [opts.fontFamily='Georgia, serif']
     * @param {number}  [opts.baseSize=18]        Base font size in grid user units
     * @param {SWColor} [opts.fillColor]           Fill; default derived from swOrange
     * @param {SWColor} [opts.strokeColor]         Border; null = no stroke
     * @param {number}  [opts.strokeWt=2]
     *
     * @param {boolean} [opts.shouldBreathe=false]
     * @param {number}  [opts.breatheAmount=5]     Amplitude in user units
     * @param {number}  [opts.breatheSpeed=1]      Cycles per second
     *
     * @param {boolean} [opts.shouldSpin=false]
     * @param {number}  [opts.spinSpeed=1.0]       Radians per second
     *
     * @param {boolean} [opts.shouldCycleHue=false]
     * @param {number}  [opts.hueCycleSpeed=30]    Hue degrees per second
     */
    constructor(symbol, bug, opts = {}) {

        // Symbol — enforce single character
        this.symbol = (typeof symbol === 'string' && symbol.length > 0)
            ? symbol.charAt(0)
            : 'A';

        // Position manager
        this.bug = bug;

        // Display
        this.fontFamily  = opts.fontFamily  || 'Georgia, serif';
        this.baseSize    = (opts.baseSize   !== undefined) ? Number(opts.baseSize)  : 18;
        this.strokeWt    = (opts.strokeWt   !== undefined) ? Number(opts.strokeWt)  : 2;

        // Colors — use SWColor.copy() so external references don't alias in
        this.fillColor   = opts.fillColor   ? SWColor.copy(opts.fillColor)   : null;
        this.strokeColor = opts.strokeColor ? SWColor.copy(opts.strokeColor) : null;

        // Breathing
        this.shouldBreathe  = opts.shouldBreathe  || false;
        this.breatheAmount  = (opts.breatheAmount !== undefined) ? Number(opts.breatheAmount) : 5;
        this.breatheSpeed   = (opts.breatheSpeed  !== undefined) ? Number(opts.breatheSpeed)  : 1;

        // Spin
        this.shouldSpin  = opts.shouldSpin  || false;
        this.spinSpeed   = (opts.spinSpeed  !== undefined) ? Number(opts.spinSpeed)  : 1.0;
        this._angle      = 0;   // accumulated rotation in radians

        // Hue cycling (applied to fillColor)
        this.shouldCycleHue = opts.shouldCycleHue || false;
        this.hueCycleSpeed  = (opts.hueCycleSpeed !== undefined) ? Number(opts.hueCycleSpeed) : 30;

        // Elapsed time in seconds (drives breathing sinusoid)
        this._etime = 0;

    }//end constructor

    // ── Update ──────────────────────────────────────────────────────────────

    /**
     * Advance all animations by dt seconds.
     * Typically called once per draw() cycle: ch.update(1 / frameRate()).
     * @param {number} dt  Elapsed seconds since last frame
     */
    update(dt) {

        this._etime += dt;

        if (this.shouldSpin) {
            this._angle += this.spinSpeed * dt;    // radians/second × seconds = radians
        }

        if (this.shouldCycleHue && this.fillColor) {
            // Advance hue and wrap around 360
            const newH = (this.fillColor.h + this.hueCycleSpeed * dt) % 360;
            // Rebuild SWColor so the p5.js color object is refreshed
            this.fillColor = new SWColor(
                newH,
                this.fillColor.s,
                this.fillColor.b,
                this.fillColor.a,
                this.fillColor.name
            );
        }

    }//end update

    // ── Draw ──────────────────────────────────────────────────────────────────

    /**
     * Render the character on the given SWGrid.
     * Must be called while the p5.js colorMode is HSB (set in setup()).
     * @param {SWGrid} grid
     */
    draw(grid) {

        // ── Compute current size in user units ──────────────────────────────
        let sizeUser = this.baseSize;
        if (this.shouldBreathe) {
            // Inline SWSinusoid: A * sin(B * (t - 0)) + C
            //   A = breatheAmount   (amplitude: peak deviation from baseSize)
            //   B = breatheSpeed * 2π  (angular frequency → cycles per second)
            //   C = baseSize         (vertical shift / rest size)
            const A = this.breatheAmount;
            const B = this.breatheSpeed * Math.PI * 2;
            const C = this.baseSize;
            sizeUser = A * Math.sin(B * this._etime) + C;
            sizeUser = Math.max(1, sizeUser);   // never zero or negative
        }

        // Convert user-unit size → screen pixels via the grid's x-scale
        // grid.xScale = screenWidth / (grid.LR.x - grid.UL.x)  [pixels per user unit]
        const screenSize = sizeUser * grid.xScale;

        // ── Convert bug position to screen coordinates ──────────────────────
        const sp = grid.userToScreen(this.bug.x, this.bug.y);

        // ── Render ───────────────────────────────────────────────────────────
        push();
        translate(sp.x, sp.y);
        if (this.shouldSpin) {
            rotate(this._angle);
        }

        textFont(this.fontFamily);
        textSize(screenSize);
        textAlign(CENTER, CENTER);

        if (this.strokeColor) {
            stroke(this.strokeColor.col);
            strokeWeight(this.strokeWt);
        } else {
            noStroke();
        }

        if (this.fillColor) {
            fill(this.fillColor.col);
        } else {
            noFill();
        }

        text(this.symbol, 0, 0);
        pop();

    }//end draw

    // ── Setters ───────────────────────────────────────────────────────────────

    setSymbol(sym) {
        if (typeof sym === 'string' && sym.length > 0) {
            this.symbol = sym.charAt(0);
        }
    }//end setSymbol

    setFontFamily(f)    { this.fontFamily   = f; }
    setBaseSize(s)      { this.baseSize     = Number(s); }
    setStrokeWt(w)      { this.strokeWt     = Number(w); }

    /** @param {SWColor|null} c */
    setFillColor(c)     { this.fillColor    = c; }

    /** @param {SWColor|null} c  — pass null to remove stroke */
    setStrokeColor(c)   { this.strokeColor  = c; }

    setShouldBreathe(b)  { this.shouldBreathe  = !!b; }
    setBreatheAmount(a)  { this.breatheAmount  = Number(a); }
    setBreatheSpeed(s)   { this.breatheSpeed   = Number(s); }

    setShouldSpin(b)     { this.shouldSpin     = !!b; }
    setSpinSpeed(s)      { this.spinSpeed      = Number(s); }

    setShouldCycleHue(b) { this.shouldCycleHue = !!b; }
    setHueCycleSpeed(s)  { this.hueCycleSpeed  = Number(s); }

    // ── Utility ───────────────────────────────────────────────────────────────

    toString() {
        return `SWCharacter('${this.symbol}', font:'${this.fontFamily}', ` +
               `size:${this.baseSize}, ` +
               `pos:(${this.bug.x.toFixed(1)}, ${this.bug.y.toFixed(1)}))`;
    }//end toString

}//end class SWCharacter
