/*
File: swEllipse.js
Date: 2026-02-28
Author: klp + GitHub Copilot
Workspace: SketchWaveTNT2026-02-28-Stg6
Purpose: SWEllipse class for SketchWaveJS
Comment(s):

SWEllipse: An ellipse with a center SWPoint, radiusX (semi-width), radiusY (semi-height),
fill color, optional border, rotation, and animation features.

Unlike SWRectangle, SWEllipse has a continuous boundary — no corner vertices.
Rotation is achieved via p5.js push/translate/rotate/pop, so it is purely visual
and does not recompute point arrays, making it efficient for complex animations.

Breathing scales radiusX and/or radiusY independently from originalRadiusX/Y.
Rotation tracks a cumulative angle (degrees, CCW in user/math convention).

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

console.log("[swEllipse.js] SWEllipse class loaded.");

class SWEllipse {
    /**
     * @param {SWPoint} center     - Center point (SWPoint instance)
     * @param {number}  radiusX   - Semi-width  (horizontal radius, user units)
     * @param {number}  radiusY   - Semi-height (vertical radius,  user units)
     * @param {SWColor} fillColor - Fill color (SWColor instance)
     * @param {Object}  [options]
     *   strokeColor  : SWColor  — border color (optional)
     *   strokeWeight : number   — border thickness (default 2)
     *   showCenter   : boolean  — show center SWPoint (default false)
     *   rotation     : number   — initial rotation in degrees CCW (default 0)
     */
    constructor(center, radiusX, radiusY, fillColor, options = {}) {
        this.center      = center;
        this.radiusX     = radiusX;
        this.radiusY     = radiusY;
        this.fillColor   = fillColor;
        this.strokeColor = options.strokeColor   || undefined;
        this.strokeWeight = options.strokeWeight !== undefined ? options.strokeWeight : 2;
        this.showCenter  = options.showCenter    !== undefined ? options.showCenter   : false;
        this.rotation    = options.rotation      || 0;  // degrees CCW

        // Set center-point styling
        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;

        // Store originals for animation & drag-reset
        this.originalRadiusX = radiusX;
        this.originalRadiusY = radiusY;
        this.originalCenter  = new SWPoint(
            center.x, center.y, undefined, cwt, cc
        );
    }//end constructor

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

    /** Area = π × radiusX × radiusY */
    get area() {
        return Math.PI * this.radiusX * this.radiusY;
    }//end area

    /**
     * Perimeter approximation (Ramanujan's second formula).
     * Exact only for circles; very accurate for most ellipses.
     */
    get perimeter() {
        const a = Math.max(this.radiusX, this.radiusY);
        const b = Math.min(this.radiusX, this.radiusY);
        return Math.PI * (3 * (a + b) - Math.sqrt((3 * a + b) * (a + 3 * b)));
    }//end perimeter

    /** radiusX / radiusY */
    get aspectRatio() {
        return this.radiusX / this.radiusY;
    }//end aspectRatio

    /** Larger of radiusX, radiusY */
    get semiMajorAxis() {
        return Math.max(this.radiusX, this.radiusY);
    }//end semiMajorAxis

    /** Smaller of radiusX, radiusY */
    get semiMinorAxis() {
        return Math.min(this.radiusX, this.radiusY);
    }//end semiMinorAxis

    /**
     * Eccentricity e = √(1 − (b/a)²).
     * e = 0 for a circle; approaches 1 as the ellipse becomes very elongated.
     */
    get eccentricity() {
        const a = this.semiMajorAxis, b = this.semiMinorAxis;
        if (a < 0.0001) return 0;
        return Math.sqrt(1 - (b * b) / (a * a));
    }//end eccentricity

    /** Distance from center to each focus along the major axis: c = √(a² − b²) */
    get focalDistance() {
        const a = this.semiMajorAxis, b = this.semiMinorAxis;
        return Math.sqrt(Math.max(0, a * a - b * b));
    }//end focalDistance

    /**
     * True when radiusX ≈ radiusY (within 1 % relative tolerance).
     */
    get isCircle() {
        const mx = Math.max(this.radiusX, this.radiusY);
        return mx < 0.0001 || Math.abs(this.radiusX - this.radiusY) < 0.01 * mx;
    }//end isCircle

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

    /** Show or hide the center SWPoint */
    setShowCenter(show = true) {
        this.showCenter = show;
    }//end setShowCenter

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

    /**
     * Draws the ellipse in screen (pixel) coordinates.
     * radiusX and radiusY are treated as pixel values.
     */
    draw() {
        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(); }

        push();
        translate(this.center.x, this.center.y);
        rotate(-this.rotation * Math.PI / 180); // p5 CW = user CCW
        ellipse(0, 0, 2 * this.radiusX, 2 * this.radiusY);
        pop();

        noStroke(); noFill(); strokeWeight(1);

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

    /**
     * Draws the ellipse in user / grid coordinates via the given SWGrid.
     * @param {SWGrid} grid
     */
    drawOnGrid(grid) {
        const { x: cx, y: cy } = grid.userToScreen(this.center.x, this.center.y);
        const rx = grid.xScale * this.radiusX;
        const ry = grid.yScale * this.radiusY;

        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(); }

        push();
        translate(cx, cy);
        rotate(-this.rotation * Math.PI / 180); // convert CCW user-angle → CW screen-angle
        ellipse(0, 0, 2 * rx, 2 * ry);
        pop();

        noStroke(); noFill(); strokeWeight(1);

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

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

    /**
     * Scales radiusX and/or radiusY using SWSinusoid instances.
     * Pass null for an axis to leave it unscaled.
     *
     * Modes:
     *   Uniform:     breathe(sin, sin, t)          — both axes share one sinusoid
     *   Horizontal:  breathe(sinX, null, t)         — only X grows/shrinks
     *   Vertical:    breathe(null, sinY, t)         — only Y grows/shrinks
     *   Independent: breathe(sinX, sinY, t)         — separate sinusoids per axis
     *
     * @param {SWSinusoid|null} sinusoidX - Horizontal scale driver (or null)
     * @param {SWSinusoid|null} sinusoidY - Vertical   scale driver (or null)
     * @param {number}          t         - Elapsed time in seconds
     */
    breathe(sinusoidX, sinusoidY, t) {
        const scaleX = sinusoidX ? sinusoidX.getValue(t) : 1.0;
        const scaleY = sinusoidY ? sinusoidY.getValue(t) : 1.0;
        this.radiusX = this.originalRadiusX * scaleX;
        this.radiusY = this.originalRadiusY * scaleY;
    }//end breathe

    /**
     * Resets radiusX/Y to their original values and restores the center position.
     * Does NOT reset rotation (handled externally by the sketch).
     */
    reset() {
        this.radiusX = this.originalRadiusX;
        this.radiusY = this.originalRadiusY;
        this.center.x = this.originalCenter.x;
        this.center.y = this.originalCenter.y;
    }//end reset

    // ── Foci helpers ──────────────────────────────────────────────────────

    /**
     * Returns the two focal points in user coordinates, accounting for rotation.
     * If isCircle, both points coincide at the center.
     * @returns {{f1: {x, y}, f2: {x, y}}}
     */
    getFociUserCoords() {
        const fd     = this.focalDistance;
        const rotRad = this.rotation * Math.PI / 180;
        const cx     = this.center.x, cy = this.center.y;

        let dx, dy;
        if (this.radiusX >= this.radiusY) {
            // Major axis is horizontal before rotation
            dx = fd * Math.cos(rotRad);
            dy = fd * Math.sin(rotRad);
        } else {
            // Major axis is vertical before rotation; rotate 90° extra
            dx = -fd * Math.sin(rotRad);
            dy =  fd * Math.cos(rotRad);
        }

        return {
            f1: { x: cx + dx, y: cy + dy },
            f2: { x: cx - dx, y: cy - dy }
        };
    }//end getFociUserCoords

    /**
     * Draws the two foci as small filled dots on a grid.
     * @param {SWGrid} grid
     * @param {number} [dotSize=8] — screen-pixel diameter of each focus dot
     * @param {SWColor} [dotColor] — fill color for the dots
     */
    drawFociOnGrid(grid, dotSize = 8, dotColor = undefined) {
        if (this.isCircle) return; // no distinct foci for a circle
        const { f1, f2 } = this.getFociUserCoords();
        const s1 = grid.userToScreen(f1.x, f1.y);
        const s2 = grid.userToScreen(f2.x, f2.y);

        if (dotColor && dotColor.col) { fill(dotColor.col); } else { fill(255, 120, 0); }
        noStroke();
        ellipse(s1.x, s1.y, dotSize, dotSize);
        ellipse(s2.x, s2.y, dotSize, dotSize);
        noFill();
    }//end drawFociOnGrid

    // ── Vertex extrema helpers ────────────────────────────────────────────

    /**
     * Returns the four vertex extrema in user coordinates, accounting for rotation.
     * These are the points at the ends of the major (X) and minor (Y) axes.
     * @returns {{ vRight, vLeft, vTop, vBottom }}
     */
    getVerticesUserCoords() {
        const rotRad = this.rotation * Math.PI / 180;
        const cx = this.center.x, cy = this.center.y;
        const rX = this.radiusX,  rY = this.radiusY;
        const cosR = Math.cos(rotRad), sinR = Math.sin(rotRad);
        return {
            vRight:  { x: cx + rX * cosR, y: cy + rX * sinR },
            vLeft:   { x: cx - rX * cosR, y: cy - rX * sinR },
            vTop:    { x: cx - rY * sinR, y: cy + rY * cosR },
            vBottom: { x: cx + rY * sinR, y: cy - rY * cosR }
        };
    }//end getVerticesUserCoords

    /**
     * Draws the four vertex extrema as small filled dots on a grid.
     * @param {SWGrid} grid
     * @param {number}  [dotSize=8]  — screen-pixel diameter
     * @param {SWColor} [dotColor]   — fill color (default: blue)
     */
    drawVerticesOnGrid(grid, dotSize = 8, dotColor = undefined) {
        const { vRight, vLeft, vTop, vBottom } = this.getVerticesUserCoords();
        if (dotColor && dotColor.col) { fill(dotColor.col); } else { fill(210, 75, 90); }
        noStroke();
        [vRight, vLeft, vTop, vBottom].forEach(v => {
            const s = grid.userToScreen(v.x, v.y);
            ellipse(s.x, s.y, dotSize, dotSize);
        });
        noFill();
    }//end drawVerticesOnGrid

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

    /**
     * Returns true if screen point (px, py) is inside or near the ellipse center
     * (within `tolerance` pixels). Used for mouse hit-testing the center 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

    /**
     * String representation
     */
    toString() {
        return `SWEllipse(center: (${this.center.x.toFixed(2)}, ${this.center.y.toFixed(2)}), ` +
               `radiusX: ${this.radiusX.toFixed(2)}, radiusY: ${this.radiusY.toFixed(2)}, ` +
               `area: ${this.area.toFixed(2)}, rotation: ${this.rotation.toFixed(1)}°)`;
    }//end toString
}//end class SWEllipse

// export default SWEllipse;
