// swTriangle.js
// SWTriangle: A triangle with 3 SWPoint vertices, centroid, fill color, optional border, and animation features
// Author: klp + GitHub Copilot
// Date: 2026-01-27
//
// Dependencies: SWPoint, SWColor, SWGrid, SWSinusoid

console.log("[swTriangle.js] SWTriangle class loaded.");

class SWTriangle {
    /**
     * @param {SWPoint} vA - First vertex (SWPoint)
     * @param {SWPoint} vB - Second vertex (SWPoint)
     * @param {SWPoint} vC - Third vertex (SWPoint)
     * @param {SWColor} fillColor - Fill color (SWColor instance)
     * @param {Object} [options] - Optional: {strokeColor, strokeWeight, showVertices, showCentroid}
     *   strokeColor: SWColor for border (optional)
     *   strokeWeight: number for border thickness (optional, default 2)
     *   showVertices: boolean (default true)
     *   showCentroid: boolean (default false)
     */
    constructor(vA, vB, vC, fillColor, options = {}) {
        this.vA = vA;
        this.vB = vB;
        this.vC = vC;
        this.fillColor = fillColor;
        this.strokeColor = options.strokeColor || undefined;
        this.strokeWeight = options.strokeWeight || 2;
        this.showVertices = options.showVertices !== undefined ? options.showVertices : true;
        this.showCentroid = options.showCentroid !== undefined ? options.showCentroid : false;
        // Set centroid strokeWeight and color based on border
        const centroidStrokeWeight = this.strokeWeight > 0 ? this.strokeWeight : 4;
        const centroidColor = (this.strokeColor && this.strokeColor.col) ? this.strokeColor : (typeof swBlack !== 'undefined' ? swBlack : new SWColor(0,0,0,100,"black"));
        this.centroid = this.computeCentroid(centroidStrokeWeight, centroidColor);
        // Store original positions for breathing/rotation
        this.originalA = SWPoint ? new SWPoint(vA.x, vA.y, vA.z, vA.strokeWeight, vA.strokeColor) : null;
        this.originalB = SWPoint ? new SWPoint(vB.x, vB.y, vB.z, vB.strokeWeight, vB.strokeColor) : null;
        this.originalC = SWPoint ? new SWPoint(vC.x, vC.y, vC.z, vC.strokeWeight, vC.strokeColor) : null;
        this.originalCentroid = new SWPoint(this.centroid.x, this.centroid.y, undefined, centroidStrokeWeight, centroidColor);
    }//end constructor

    /**
     * Computes the centroid of the triangle
     * @returns {SWPoint} The centroid as a new SWPoint
     */
    /**
     * Computes the centroid of the triangle
     * @param {number} [strokeWeight=4] - Stroke weight for centroid point
     * @param {SWColor} [strokeColor=swBlack] - Stroke color for centroid point
     * @returns {SWPoint} The centroid as a new SWPoint
     */
    computeCentroid(strokeWeight = 4, strokeColor = (typeof swBlack !== 'undefined' ? swBlack : new SWColor(0,0,0,100,"black"))) {
        const x = (this.vA.x + this.vB.x + this.vC.x) / 3;
        const y = (this.vA.y + this.vB.y + this.vC.y) / 3;
        return new SWPoint(x, y, undefined, strokeWeight, strokeColor);
    }//end computeCentroid

    /**
     * Getter for side AB length
     * @returns {number} Distance from vertex A to vertex B
     */
    get sideAB() {
        return this.vA.distanceTo(this.vB);
    }//end sideAB

    /**
     * Getter for side BC length
     * @returns {number} Distance from vertex B to vertex C
     */
    get sideBC() {
        return this.vB.distanceTo(this.vC);
    }//end sideBC

    /**
     * Getter for side CA length
     * @returns {number} Distance from vertex C to vertex A
     */
    get sideCA() {
        return this.vC.distanceTo(this.vA);
    }//end sideCA

    /**
     * Getter for triangle perimeter
     * @returns {number} Sum of all three side lengths
     */
    get perimeter() {
        return this.sideAB + this.sideBC + this.sideCA;
    }//end perimeter

    /**
     * Getter for triangle area using cross product formula
     * @returns {number} Area of the triangle
     */
    get area() {
        return Math.abs(
            this.vA.x * (this.vB.y - this.vC.y) +
            this.vB.x * (this.vC.y - this.vA.y) +
            this.vC.x * (this.vA.y - this.vB.y)
        ) / 2;
    }//end area

    /**
     * Getter for angle at vertex A (in degrees) using law of cosines
     * @returns {number} Angle at vertex A in degrees
     */
    get angleA() {
        const a = this.sideBC; // side opposite to vertex A
        const b = this.sideCA;
        const c = this.sideAB;
        // Law of cosines: cos(A) = (b² + c² - a²) / (2bc)
        const cosA = (b * b + c * c - a * a) / (2 * b * c);
        return Math.acos(Math.max(-1, Math.min(1, cosA))) * 180 / Math.PI;
    }//end angleA

    /**
     * Getter for angle at vertex B (in degrees) using law of cosines
     * @returns {number} Angle at vertex B in degrees
     */
    get angleB() {
        const a = this.sideBC;
        const b = this.sideCA; // side opposite to vertex B
        const c = this.sideAB;
        // Law of cosines: cos(B) = (a² + c² - b²) / (2ac)
        const cosB = (a * a + c * c - b * b) / (2 * a * c);
        return Math.acos(Math.max(-1, Math.min(1, cosB))) * 180 / Math.PI;
    }//end angleB

    /**
     * Getter for angle at vertex C (in degrees) using law of cosines
     * @returns {number} Angle at vertex C in degrees
     */
    get angleC() {
        const a = this.sideBC;
        const b = this.sideCA;
        const c = this.sideAB; // side opposite to vertex C
        // Law of cosines: cos(C) = (a² + b² - c²) / (2ab)
        const cosC = (a * a + b * b - c * c) / (2 * a * b);
        return Math.acos(Math.max(-1, Math.min(1, cosC))) * 180 / Math.PI;
    }//end angleC

    /**
     * Getter to check if triangle is a right triangle (has a 90° angle)
     * @returns {boolean} True if one angle is approximately 90°
     */
    get isRightTriangle() {
        const tolerance = 0.5; // degrees
        return Math.abs(this.angleA - 90) < tolerance ||
               Math.abs(this.angleB - 90) < tolerance ||
               Math.abs(this.angleC - 90) < tolerance;
    }//end isRightTriangle

    /**
     * Getter to check if triangle is an obtuse triangle (has an angle > 90°)
     * @returns {boolean} True if one angle is greater than 90°
     */
    get isObtuseTriangle() {
        return this.angleA > 90 || this.angleB > 90 || this.angleC > 90;
    }//end isObtuseTriangle

    /**
     * Getter to check if triangle is an acute triangle (all angles < 90°)
     * @returns {boolean} True if all angles are less than 90°
     */
    get isAcuteTriangle() {
        return this.angleA < 90 && this.angleB < 90 && this.angleC < 90;
    }//end isAcuteTriangle

    /**
     * Getter to check if triangle is equilateral (all sides equal)
     * @returns {boolean} True if all sides are approximately equal
     */
    get isEquilateral() {
        const tolerance = 0.01; // relative tolerance
        const avg = (this.sideAB + this.sideBC + this.sideCA) / 3;
        return Math.abs(this.sideAB - avg) < tolerance * avg &&
               Math.abs(this.sideBC - avg) < tolerance * avg &&
               Math.abs(this.sideCA - avg) < tolerance * avg;
    }//end isEquilateral

    /**
     * Getter to check if triangle is isosceles (at least two sides equal)
     * @returns {boolean} True if at least two sides are approximately equal
     */
    get isIsosceles() {
        const tolerance = 0.01; // relative tolerance
        const ab = this.sideAB;
        const bc = this.sideBC;
        const ca = this.sideCA;
        const maxSide = Math.max(ab, bc, ca);
        const tol = tolerance * maxSide;
        return Math.abs(ab - bc) < tol ||
               Math.abs(bc - ca) < tol ||
               Math.abs(ca - ab) < tol;
    }//end isIsosceles

    /**
     * Getter to check if triangle is a 30-60-90 triangle
     * @returns {boolean} True if angles are approximately 30°, 60°, and 90°
     */
    get is30_60_90() {
        const tolerance = 0.5; // degrees
        const angles = [this.angleA, this.angleB, this.angleC].sort((a, b) => a - b);
        return Math.abs(angles[0] - 30) < tolerance &&
               Math.abs(angles[1] - 60) < tolerance &&
               Math.abs(angles[2] - 90) < tolerance;
    }//end is30_60_90

    /**
     * Draws the triangle in screen coordinates (p5.js)
     */
    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();
        }
        triangle(this.vA.x, this.vA.y, this.vB.x, this.vB.y, this.vC.x, this.vC.y);
        noStroke();
        noFill();
        // Optionally show vertices
        if (this.showVertices) {
            this.vA.draw();
            this.vB.draw();
            this.vC.draw();
        }
        // Optionally show centroid
        if (this.showCentroid) {
            this.centroid.draw();
        }
    }//end draw

    /**
     * Draws the triangle in user (grid) coordinates
     * @param {SWGrid} grid - The grid for mapping user to screen coordinates
     */
    drawOnGrid(grid) {
        // Map vertices to screen
        const a = grid.userToScreen(this.vA.x, this.vA.y);
        const b = grid.userToScreen(this.vB.x, this.vB.y);
        const c = grid.userToScreen(this.vC.x, this.vC.y);
        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();
        }
        triangle(a.x, a.y, b.x, b.y, c.x, c.y);
        noStroke();
        noFill();
        // Optionally show vertices
        if (this.showVertices) {
            this.vA.drawOnGrid(grid);
            this.vB.drawOnGrid(grid);
            this.vC.drawOnGrid(grid);
        }
        // Optionally show centroid
        if (this.showCentroid) {
            this.centroid.drawOnGrid(grid);
        }
    }//end drawOnGrid

    /**
     * Rotates the triangle about its centroid by the given angle (degrees, CCW)
     * @param {number} angleDeg - Angle in degrees
     */
    rotateAboutCentroidBy(angleDeg) {
        const angleRad = angleDeg * Math.PI / 180;
        const cx = this.centroid.x;
        const cy = this.centroid.y;
        // Rotate each vertex (from original positions)
        [this.vA, this.vB, this.vC].forEach((v, i) => {
            const orig = [this.originalA, this.originalB, this.originalC][i];
            const dx = orig.x - cx;
            const dy = orig.y - cy;
            v.x = cx + dx * Math.cos(angleRad) - dy * Math.sin(angleRad);
            v.y = cy + dx * Math.sin(angleRad) + dy * Math.cos(angleRad);
        });
        // Update centroid with correct styling
        const centroidStrokeWeight = this.strokeWeight > 0 ? this.strokeWeight : 4;
        const centroidColor = (this.strokeColor && this.strokeColor.col) ? this.strokeColor : (typeof swBlack !== 'undefined' ? swBlack : new SWColor(0,0,0,100,"black"));
        this.centroid = this.computeCentroid(centroidStrokeWeight, centroidColor);
    }//end rotateAboutCentroidBy

    /**
     * Animates rotation about the centroid, similar to SWLine's rotateAboutMidPoint.
     * @param {number} degPerSec - Angular velocity in degrees per second (CCW)
     * @param {number} t - Time in seconds (or frameCount/framerate)
     *
     * Usage: triangle.rotateAboutCentroid(degPerSec, t);
     */
    rotateAboutCentroid(degPerSec, t) {
        const angleDeg = degPerSec * t;
        const angleRad = angleDeg * Math.PI / 180;
        // Always rotate from original positions about the original centroid
        const cx = this.originalCentroid.x;
        const cy = this.originalCentroid.y;
        [this.vA, this.vB, this.vC].forEach((v, i) => {
            const orig = [this.originalA, this.originalB, this.originalC][i];
            const dx = orig.x - cx;
            const dy = orig.y - cy;
            v.x = cx + dx * Math.cos(angleRad) - dy * Math.sin(angleRad);
            v.y = cy + dx * Math.sin(angleRad) + dy * Math.cos(angleRad);
        });
        // Update centroid with correct styling
        const centroidStrokeWeight = this.strokeWeight > 0 ? this.strokeWeight : 4;
        const centroidColor = (this.strokeColor && this.strokeColor.col) ? this.strokeColor : (typeof swBlack !== 'undefined' ? swBlack : new SWColor(0,0,0,100,"black"));
        this.centroid = this.computeCentroid(centroidStrokeWeight, centroidColor);
    }//end rotateAboutCentroid

    /**
     * Makes the triangle 'breathe' by moving each vertex radially from the centroid using a SWSinusoid
     * @param {SWSinusoid} sinusoid - The sinusoid controlling the breathing
     * @param {number} t - Time parameter (seconds)
     *
     * Each vertex moves along the line from centroid to original vertex, distance modulated by sinusoid
     */
    breathe(sinusoid, t) {
        const cx = this.originalCentroid.x;
        const cy = this.originalCentroid.y;
        const minScale = 0.1; // Prevent collapse
        // Get scale factor directly from sinusoid
        let scale = sinusoid.getValue(t);
        if (scale < minScale) scale = minScale;
        // Debug logging
        console.log(`[breathe] t=${t.toFixed(2)} scale=${scale.toFixed(2)}`);
        [this.vA, this.vB, this.vC].forEach((v, i) => {
            const orig = [this.originalA, this.originalB, this.originalC][i];
            const dx = orig.x - cx;
            const dy = orig.y - cy;
            v.x = cx + dx * scale;
            v.y = cy + dy * scale;
        });
        // Update centroid with correct styling
        const centroidStrokeWeight = this.strokeWeight > 0 ? this.strokeWeight : 4;
        const centroidColor = (this.strokeColor && this.strokeColor.col) ? this.strokeColor : (typeof swBlack !== 'undefined' ? swBlack : new SWColor(0,0,0,100,"black"));
        this.centroid = this.computeCentroid(centroidStrokeWeight, centroidColor);
    }//end breathe

    /**
     * Applies breathing (scaling), rotation, or both to the triangle, starting from the original vertices.
     * This mimics the transform method in SWLine for consistency and composability.
     *
     * Call this in your animation loop to combine effects without interference.
     *
     * @param {Object} options - { sinusoid, t, degPerSec }
     *   sinusoid: SWSinusoid instance (optional, for breathing)
     *   t: time in seconds
     *   degPerSec: angular velocity (optional, for rotation)
     *
     * Usage:
     *   triangle.transform({sinusoid, t, degPerSec}); // both
     *   triangle.transform({sinusoid, t}); // just breathing
     *   triangle.transform({degPerSec, t}); // just rotation
     *
     * Implementation notes:
     * - Always start from the original vertices and centroid for consistent animation.
     * - Breathing: scale all vertices radially from the centroid by a single scale factor.
     * - Rotation: rotate all vertices about the centroid by the given angle.
     * - Order: Breathing (scaling) is applied first, then rotation.
     * - This ensures the centroid remains invariant and the animation is robust.
     */
    transform({sinusoid = null, t = 0, degPerSec = null} = {}) {
        // 1. Start from original vertices and centroid
        const cx = this.originalCentroid.x;
        const cy = this.originalCentroid.y;
        // 2. Compute scale factor for breathing (if provided)
        let scale = 1;
        if (sinusoid) {
            // Get scale factor directly from sinusoid
            scale = sinusoid.getValue(t);
            const minScale = 0.1;
            if (scale < minScale) scale = minScale;
            // Debug: log breathing info
            // console.log(`[transform] t=${t.toFixed(2)} scale=${scale.toFixed(2)}`);
        }
        // 3. Compute rotation angle (if provided)
        let angleRad = 0;
        if (degPerSec !== null) {
            const angleDeg = degPerSec * t;
            angleRad = angleDeg * Math.PI / 180;
        }
        // 4. For each vertex, apply breathing (scaling) and then rotation about centroid
        [this.vA, this.vB, this.vC].forEach((v, i) => {
            // Vector from centroid to original vertex
            const orig = [this.originalA, this.originalB, this.originalC][i];
            let dx = orig.x - cx;
            let dy = orig.y - cy;
            // Breathing: scale vector
            dx *= scale;
            dy *= scale;
            // Rotation: rotate vector by angleRad
            const dxRot = dx * Math.cos(angleRad) - dy * Math.sin(angleRad);
            const dyRot = dx * Math.sin(angleRad) + dy * Math.cos(angleRad);
            // Set new vertex position
            v.x = cx + dxRot;
            v.y = cy + dyRot;
        });
        // 5. Update centroid with correct styling (should remain invariant)
        const centroidStrokeWeight = this.strokeWeight > 0 ? this.strokeWeight : 4;
        const centroidColor = (this.strokeColor && this.strokeColor.col) ? this.strokeColor : (typeof swBlack !== 'undefined' ? swBlack : new SWColor(0,0,0,100,"black"));
        this.centroid = this.computeCentroid(centroidStrokeWeight, centroidColor);
    }//end transform

    /**
     * Resets the triangle to its original vertex positions
     */
    reset() {
        [this.vA, this.vB, this.vC].forEach((v, i) => {
            const orig = [this.originalA, this.originalB, this.originalC][i];
            v.x = orig.x;
            v.y = orig.y;
        });
        // Update centroid with correct styling
        const centroidStrokeWeight = this.strokeWeight > 0 ? this.strokeWeight : 4;
        const centroidColor = (this.strokeColor && this.strokeColor.col) ? this.strokeColor : (typeof swBlack !== 'undefined' ? swBlack : new SWColor(0,0,0,100,"black"));
        this.centroid = this.computeCentroid(centroidStrokeWeight, centroidColor);
    }//end reset

    /**
     * Set whether to show vertices
     * @param {boolean} show
     */
    setShowVertices(show = true) {
        this.showVertices = show;
    }//end setShowVertices

    /**
     * Set whether to show centroid
     * @param {boolean} show
     */
    setShowCentroid(show = true) {
        this.showCentroid = show;
    }//end setShowCentroid

    /**
     * String representation
     */
    toString() {
        return `SWTriangle(vA: ${this.vA.toString()}, vB: ${this.vB.toString()}, vC: ${this.vC.toString()}, centroid: ${this.centroid.toString()}, fillColor: ${this.fillColor.toString()})`;
    }//end toString

    /**
     * Shifts the triangle horizontally by xInc (modifies all vertices and updates centroid)
     * @param {number} xInc - Amount to shift in x direction
     */
    horizShiftBy(xInc) {
        // Shift all vertices
        this.vA.x += xInc;
        this.vB.x += xInc;
        this.vC.x += xInc;
        // Also shift originals for animation consistency
        if (this.originalA && this.originalB && this.originalC) {
            this.originalA.x += xInc;
            this.originalB.x += xInc;
            this.originalC.x += xInc;
        }
        // Update centroid and originalCentroid
        const centroidStrokeWeight = this.strokeWeight > 0 ? this.strokeWeight : 4;
        const centroidColor = (this.strokeColor && this.strokeColor.col) ? this.strokeColor : (typeof swBlack !== 'undefined' ? swBlack : new SWColor(0,0,0,100,"black"));
        this.centroid = this.computeCentroid(centroidStrokeWeight, centroidColor);
        if (this.originalCentroid) {
            this.originalCentroid.x += xInc;
        }
    }//end horizShiftBy
}//end class SWTriangle

// Export for module use (if needed)
// export default SWTriangle;
