/*
File:    swRegularPolygon.js
Date:    2026-02-28
Author:  klp + GitHub Copilot
App:     SketchWaveTNT2026-02-28-Stg6
Purpose: SWRegularPolygon — a regular n-sided polygon with optional rounded
         corners, breathing, and rotation animation.

  A regular polygon is defined by a center SWPoint, a circumradius (the distance
  from the center to any vertex), and a number of sides (≥ 3).  Vertices are
  computed on the fly from these three values plus the current animation state —
  no stored vertex array is needed between frames.

  Two drawing modes are supported:
    sharp   (cornerRadius = 0) — uses beginShape / vertex / endShape(CLOSE)
    rounded (cornerRadius > 0) — uses quadratic bezier corner chamfering: each
                                  vertex is cut at `cornerRadius` user-units along
                                  both adjacent edges, producing a smooth arc whose
                                  size scales linearly with the slider value.

  Breathing scales the circumradius uniformly via a single SWSinusoid,
  stored as _scaleR.  Because all vertices are equidistant from the center,
  single-axis scaling has the natural meaning of pulsing the whole polygon.

  Rotation accumulates in _animRotDeg (degrees CCW, added to the static
  `rotation` property at draw time), following the same pattern as
  SWRoundedRectangle.

  The first vertex is always placed at the top (-90°) before any rotation is
  applied, so a triangle points upward, a square is axis-aligned, etc.

  Dependencies: p5.js, SWColor, SWPoint, SWGrid, SWSinusoid
*/

console.log("[swRegularPolygon.js] SWRegularPolygon class loaded.");

class SWRegularPolygon {

    /**
     * @param {SWPoint} center      — Center of the polygon (user coordinates)
     * @param {number}  radius      — Circumradius in user units (center → vertex)
     * @param {number}  numSides    — Number of sides (integer ≥ 3)
     * @param {SWColor} fillColor   — Interior fill color
     * @param {Object}  [options]
     *   cornerRadius  : number  — rounding amount; 0 = sharp corners (default)
     *                             values > 0 use curveVertex spline rounding
     *   strokeColor   : SWColor — border color (optional)
     *   strokeWeight  : number  — border thickness (default 2)
     *   showCenter    : boolean — show center SWPoint (default false)
     *   showVertices  : boolean — show a dot at each vertex (default false)
     *   rotation      : number  — initial rotation in degrees CCW (default 0)
     */
    constructor(center, radius, numSides, fillColor, options = {}) {
        this.center       = center;
        this.radius       = radius;
        this.numSides     = Math.max(3, Math.round(numSides));
        this.fillColor    = fillColor;

        this.cornerRadius = options.cornerRadius  !== undefined ? options.cornerRadius  : 0;
        this.strokeColor  = options.strokeColor   || undefined;
        this.strokeWeight = options.strokeWeight  !== undefined ? options.strokeWeight  : 2;
        this.showCenter   = options.showCenter    !== undefined ? options.showCenter    : false;
        this.showVertices = options.showVertices  !== undefined ? options.showVertices  : false;
        this.rotation     = options.rotation      || 0; // degrees CCW

        // Style the center SWPoint
        const cwt = this.strokeWeight > 0 ? this.strokeWeight : 4;
        const cc  = (this.strokeColor && this.strokeColor.col)
                        ? this.strokeColor
                        : (typeof swBlack !== 'undefined' ? swBlack : new SWColor(0, 0, 0, 100, "black"));
        this.center.strokeWeight = cwt;
        this.center.strokeColor  = cc;

        // ── Animation state ────────────────────────────────────────────────────
        this._scaleR     = 1;  // breathing scale for the circumradius
        this._animRotDeg = 0;  // extra rotation from animation (degrees CCW)

        // ── Originals for reset ────────────────────────────────────────────────
        this.originalRadius = radius;
        this.originalCenter = new SWPoint(center.x, center.y, undefined, cwt, cc);
    }//end constructor

    // ── Geometric getters ──────────────────────────────────────────────────────

    /** Circumradius at the current breathing scale */
    get currentRadius() {
        return this.radius * this._scaleR;
    }//end currentRadius

    /** Length of one side at the current scale: 2r · sin(π/n) */
    get sideLength() {
        return 2 * this.currentRadius * Math.sin(Math.PI / this.numSides);
    }//end sideLength

    /** Apothem (inradius): perpendicular distance from center to a side midpoint */
    get apothem() {
        return this.currentRadius * Math.cos(Math.PI / this.numSides);
    }//end apothem

    /** Area: ½ · n · r² · sin(2π/n) */
    get area() {
        const r = this.currentRadius;
        return 0.5 * this.numSides * r * r * Math.sin(2 * Math.PI / this.numSides);
    }//end area

    /** Perimeter: n · sideLength */
    get perimeter() {
        return this.numSides * this.sideLength;
    }//end perimeter

    /** Each interior angle in degrees: (n − 2) · 180 / n */
    get interiorAngle() {
        return ((this.numSides - 2) * 180) / this.numSides;
    }//end interiorAngle

    /** Each exterior angle in degrees: 360 / n */
    get exteriorAngle() {
        return 360 / this.numSides;
    }//end exteriorAngle

    // ── Vertex computation ─────────────────────────────────────────────────────

    /**
     * Returns all vertex positions in user (grid) coordinates, accounting for
     * the current circumradius scale and total rotation.
     * @returns {Array<{x: number, y: number}>}
     */
    _computeUserVertices() {
        const r         = this.currentRadius;
        const totalRad  = (this.rotation + this._animRotDeg) * Math.PI / 180;
        const baseAngle = -Math.PI / 2; // first vertex at top before rotation
        return Array.from({ length: this.numSides }, (_, i) => {
            const angle = baseAngle + totalRad + (2 * Math.PI * i / this.numSides);
            return {
                x: this.center.x + r * Math.cos(angle),
                y: this.center.y + r * Math.sin(angle)
            };
        });
    }//end _computeUserVertices

    // ── Drawing helpers ────────────────────────────────────────────────────────

    _applyFillStroke() {
        if (this.fillColor   && this.fillColor.col)   { fill(this.fillColor.col);   } else { noFill();   }
        if (this.strokeColor && this.strokeColor.col) {
            stroke(this.strokeColor.col);
            strokeWeight(this.strokeWeight);
        } else { noStroke(); }
    }//end _applyFillStroke

    /** Sharp-corner draw: plain vertex path. */
    _drawSharp(pts) {
        beginShape();
        for (const p of pts) vertex(p.x, p.y);
        endShape(CLOSE);
    }//end _drawSharp

    /**
     * Rounded-corner draw: quadratic bezier chamfering.
     *
     * For each vertex, two points are placed `pixelRadius` px along each adjacent
     * edge (clamped to 45% of the shorter edge so corners never overlap). A
     * quadraticVertex() arc is drawn through the original vertex as the control
     * point, producing a smooth, consistently-sized rounded corner whose radius
     * scales directly with the `cornerRadius` property.
     *
     * @param {Array<{x,y}>} pts         — Screen-space vertex positions
     * @param {number}       pixelRadius — Corner radius in pixels
     */
    _drawRounded(pts, pixelRadius) {
        const n = pts.length;
        beginShape();
        for (let i = 0; i < n; i++) {
            const prev = pts[(i - 1 + n) % n];
            const curr = pts[i];
            const next = pts[(i + 1) % n];

            // Vectors from curr toward the adjacent vertices
            const dx1 = prev.x - curr.x,  dy1 = prev.y - curr.y;
            const len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
            const dx2 = next.x - curr.x,  dy2 = next.y - curr.y;
            const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);

            // Clamp so two adjacent chamfers never exceed 45% of their shared edge
            const r1 = Math.min(pixelRadius, len1 * 0.45);
            const r2 = Math.min(pixelRadius, len2 * 0.45);

            // Cut points on each edge
            const p1 = { x: curr.x + (dx1 / len1) * r1, y: curr.y + (dy1 / len1) * r1 };
            const p2 = { x: curr.x + (dx2 / len2) * r2, y: curr.y + (dy2 / len2) * r2 };

            vertex(p1.x, p1.y);                            // arrive at cut point
            quadraticVertex(curr.x, curr.y, p2.x, p2.y);  // arc around the corner
        }
        endShape(CLOSE);
    }//end _drawRounded

    // ── Rendering ─────────────────────────────────────────────────────────────

    /**
     * Draws using raw pixel coordinates (no grid).
     * `center.x/y` and `radius` are treated as pixel values.
     */
    draw() {
        const r         = this.currentRadius;
        const totalRad  = (this.rotation + this._animRotDeg) * Math.PI / 180;
        const baseAngle = -Math.PI / 2;
        const pts = Array.from({ length: this.numSides }, (_, i) => {
            const angle = baseAngle + totalRad + (2 * Math.PI * i / this.numSides);
            return { x: this.center.x + r * Math.cos(angle),
                     y: this.center.y + r * Math.sin(angle) };
        });
        this._applyFillStroke();
        if (this.cornerRadius > 0) this._drawRounded(pts, this.cornerRadius); else this._drawSharp(pts);
        noStroke(); noFill(); strokeWeight(1);
        if (this.showCenter && this.center && this.center.draw) this.center.draw();
    }//end draw

    /**
     * Draws mapped through an SWGrid instance (user → screen coordinates).
     * @param {SWGrid} grid
     */
    drawOnGrid(grid) {
        const userVerts = this._computeUserVertices();
        const pts       = userVerts.map(v => grid.userToScreen(v.x, v.y));

        this._applyFillStroke();
        if (this.cornerRadius > 0) {
            this._drawRounded(pts, this.cornerRadius * grid.xScale);
        } else {
            this._drawSharp(pts);
        }
        noStroke(); noFill(); strokeWeight(1);

        if (this.showVertices) {
            const dotFill = (this.strokeColor && this.strokeColor.col)
                                ? this.strokeColor.col : color(40, 100, 100);
            fill(dotFill);
            noStroke();
            for (const p of pts) ellipse(p.x, p.y, 8, 8);
            noFill();
        }

        if (this.showCenter && this.center && this.center.drawOnGrid) {
            this.center.drawOnGrid(grid);
        }
    }//end drawOnGrid

    // ── Animation ─────────────────────────────────────────────────────────────

    /**
     * Scales the circumradius using a SWSinusoid.
     * @param {SWSinusoid|null} sinusoid — Radius scale driver (or null)
     * @param {number}          t        — Elapsed time in seconds
     */
    breathe(sinusoid, t) {
        this._scaleR = sinusoid ? Math.max(0.1, sinusoid.getValue(t)) : 1;
    }//end breathe

    /**
     * Rotates the polygon about its center.
     * @param {number} degPerSec — angular velocity in °/s (CCW positive)
     * @param {number} t         — elapsed time in seconds
     */
    rotateAboutCenter(degPerSec, t) {
        this._animRotDeg = degPerSec * t;
    }//end rotateAboutCenter

    /**
     * Applies breathing and rotation in one call.
     * @param {Object} options — { sinusoid, t, degPerSec }
     */
    transform({ sinusoid = null, t = 0, degPerSec = null } = {}) {
        this._scaleR     = sinusoid ? Math.max(0.1, sinusoid.getValue(t)) : 1;
        this._animRotDeg = degPerSec !== null ? degPerSec * t : 0;
    }//end transform

    /**
     * Resets the polygon to its original radius, center, and rotation state.
     * Does NOT reset the static `rotation` property.
     */
    reset() {
        this.radius      = this.originalRadius;
        this._scaleR     = 1;
        this._animRotDeg = 0;
        this.center.x    = this.originalCenter.x;
        this.center.y    = this.originalCenter.y;
    }//end reset

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

    /**
     * Returns the n vertex positions in user coordinates, accounting for
     * current breathing scale and total rotation.
     * @returns {Array<{x: number, y: number}>}
     */
    getVerticesUserCoords() {
        return this._computeUserVertices();
    }//end getVerticesUserCoords

    /**
     * Draws a small filled dot at each vertex on the grid.
     * @param {SWGrid}  grid
     * @param {number}  [dotSize=8]  — screen-pixel diameter
     * @param {SWColor} [dotColor]   — fill color (default: stroke color or blue)
     */
    drawVerticesOnGrid(grid, dotSize = 8, dotColor = undefined) {
        const verts = this._computeUserVertices();
        const fillCol = (dotColor && dotColor.col)
                            ? dotColor.col
                            : (this.strokeColor && this.strokeColor.col)
                                ? this.strokeColor.col : color(40, 100, 100);
        fill(fillCol);
        noStroke();
        for (const v of verts) {
            const s = grid.userToScreen(v.x, v.y);
            ellipse(s.x, s.y, dotSize, dotSize);
        }
        noFill();
    }//end drawVerticesOnGrid

    /**
     * Returns true if screen point (px, py) is within `tolerance` pixels of the
     * polygon's center — used for mouse hit-testing the center drag handle.
     * @param {number} px
     * @param {number} py
     * @param {SWGrid} grid
     * @param {number} [tolerance=12]
     */
    centerContainsPoint(px, py, grid, tolerance = 12) {
        const { x: cx, y: cy } = grid.userToScreen(this.center.x, this.center.y);
        const dx = px - cx, dy = py - cy;
        return (dx * dx + dy * dy) <= tolerance * tolerance;
    }//end centerContainsPoint

    toString() {
        const totalDeg = this.rotation + this._animRotDeg;
        return `SWRegularPolygon(center: (${this.center.x.toFixed(2)}, ${this.center.y.toFixed(2)}), ` +
               `radius: ${this.radius.toFixed(2)}, sides: ${this.numSides}, ` +
               `sideLength: ${this.sideLength.toFixed(2)}, area: ${this.area.toFixed(2)}, ` +
               `rotation: ${totalDeg.toFixed(1)}\u00b0)`;
    }//end toString

}//end class SWRegularPolygon
