// swGradient.js
// SWGradient: Unified gradient class for drawing linear gradients
// Author: klp + GitHub Copilot
// Date: 2026-02-16
//
// Draws smooth linear gradients at any angle:
// - On the full canvas (screen coordinates)
// - On a full grid (user coordinates)
// - Within a custom box on a grid (user coordinates)
//
// Dependencies: SWColor, SWPoint, SWGrid, p5.js

console.log("[swGradient.js] SWGradient class loaded.");

class SWGradient {
    /**
     * Creates a new gradient with two colors and optional rotation angles.
     * 
     * TWO-ANGLE SYSTEM:
     * This class manages two independent rotation angles:
     * 
     * 1. angleDeg (gradient direction):
     *    - Controls which direction the gradient flows WITHIN its space
     *    - 0° = vertical (top to bottom)
     *    - 90° = horizontal (left to right)
     *    - Positive values rotate counterclockwise
     *    - Used by ALL drawing methods (draw, drawOnGrid, drawBoxOnGrid)
     * 
     * 2. boxAngleDeg (box rotation):
     *    - Controls rotation of the ENTIRE BOX shape in space
     *    - Only relevant for drawBoxOnGrid() method
     *    - 0° = no rotation (box aligned with grid axes)
     *    - Positive = counterclockwise, negative = clockwise
     *    - Independent of gradient direction
     * 
     * EXAMPLE:
     * // Create vertical gradient (0°) in a box rotated 45°
     * let grad = new SWGradient(swRed, swBlue, 0, 45);
     * // Result: Red-to-blue flows vertically, but box is tilted 45°
     * 
     * @param {SWColor} topSWColor - Color at the start of the gradient (angle=0: top)
     * @param {SWColor} bottomSWColor - Color at the end of the gradient (angle=0: bottom)
     * @param {number} [angleDeg=0] - Angle of gradient direction in degrees (0=vertical, 90=horizontal, positive=CCW)
     * @param {number} [boxAngleDeg=0] - Default rotation angle for boxes in degrees (positive=CCW, negative=CW)
     */
    constructor(topSWColor, bottomSWColor, angleDeg = 0, boxAngleDeg = 0) {
        this.topSWColor = topSWColor;
        this.bottomSWColor = bottomSWColor;
        
        // Gradient direction angle (how colors flow)
        this.angleDeg = angleDeg;
        
        // Box rotation angle (how box is rotated in space)
        // Stored as property for convenience, but can be overridden per-draw
        this.boxAngleDeg = boxAngleDeg;
    }

    /**
     * Interpolates between two SWColors using HSB color space with hue wraparound
     * @param {number} t - Interpolation parameter (0 = topColor, 1 = bottomColor)
     * @returns {p5.Color} - Interpolated color
     * @private
     */
    _interpolateHSB(t) {
        // Clamp t to [0, 1]
        t = Math.max(0, Math.min(1, t));
        
        // Handle hue wraparound for smooth color transitions
        let h1 = this.topSWColor.h;
        let h2 = this.bottomSWColor.h;
        let dh = h2 - h1;
        
        // If hue difference > 180°, go the "short way" around the color wheel
        if (Math.abs(dh) > 180) {
            if (dh > 0) {
                h1 += 360;
            } else {
                h2 += 360;
            }
        }
        
        // Interpolate all channels
        let h = (1 - t) * h1 + t * h2;
        h = ((h % 360) + 360) % 360; // Normalize to [0, 360)
        let s = (1 - t) * this.topSWColor.s + t * this.bottomSWColor.s;
        let b = (1 - t) * this.topSWColor.b + t * this.bottomSWColor.b;
        let a = (1 - t) * this.topSWColor.a + t * this.bottomSWColor.a;
        
        return color(h, s, b, a);
    }

    /**
     * Draws the gradient on the current p5.js canvas at the specified angle
     * @param {number} [x=0] - x position (default 0)
     * @param {number} [y=0] - y position (default 0)
     * @param {number} [w=width] - width of gradient (default canvas width)
     * @param {number} [h=height] - height of gradient (default canvas height)
     */
    draw(x = 0, y = 0, w = width, h = height) {
        // Four corners of the area
        const UL = {x: x, y: y};
        const UR = {x: x + w, y: y};
        const LL = {x: x, y: y + h};
        const LR = {x: x + w, y: y + h};
        
        // Compute gradient direction vector from angle (in radians)
        // 0deg = vertical (top to bottom), 90deg = horizontal (left to right), angles increase CCW
        const angleRad = (this.angleDeg || 0) * Math.PI / 180;
        const dx = Math.sin(angleRad);
        const dy = Math.cos(angleRad);
        
        // Project all corners onto the gradient axis to find min/max
        const projections = [UL, UR, LL, LR].map(pt => pt.x * dx + pt.y * dy);
        const minProj = Math.min(...projections);
        const maxProj = Math.max(...projections);
        
        // Use the bounding box diagonal as the number of steps for robust coverage
        const bboxDiag = Math.ceil(Math.hypot(UR.x - LL.x, UR.y - LL.y) + Math.hypot(UL.x - LR.x, UL.y - LR.y));
        
        // Center of the area
        const cx = (UL.x + UR.x + LL.x + LR.x) / 4;
        const cy = (UL.y + UR.y + LL.y + LR.y) / 4;
        
        colorMode(HSB, 360, 100, 100, 100);
        
        for (let i = 0; i <= bboxDiag; i++) {
            let t = i / bboxDiag;
            let gradCol = this._interpolateHSB(t);
            
            noStroke();
            fill(gradCol);
            
            // For each step, draw a strip that covers the bounding box
            let proj = minProj + t * (maxProj - minProj);
            let centerOnLine = {
                x: cx + (proj - (cx * dx + cy * dy)) * dx,
                y: cy + (proj - (cx * dx + cy * dy)) * dy
            };
            
            push();
            translate(centerOnLine.x, centerOnLine.y);
            rotate(angleRad + Math.PI / 2); // Rotate rect to be perpendicular to gradient direction
            rectMode(CENTER);
            rect(0, 0, bboxDiag + 4, 2);
            pop();
        }
    }

    /**
     * Draws the gradient filling the entire grid area at the specified angle
     * @param {SWGrid} grid - The grid to use for mapping user to screen coordinates
     */
    drawOnGrid(grid) {
        // Get the four corners of the grid in screen coordinates
        const UL = grid.userToScreen(grid.UL.x, grid.UL.y);
        const UR = grid.userToScreen(grid.LR.x, grid.UL.y);
        const LL = grid.userToScreen(grid.UL.x, grid.LR.y);
        const LR = grid.userToScreen(grid.LR.x, grid.LR.y);
        
        // Compute gradient direction vector from angle (in radians)
        // 0deg = vertical (top to bottom), 90deg = horizontal (left to right), angles increase CCW
        const angleRad = (this.angleDeg || 0) * Math.PI / 180;
        const dx = Math.sin(angleRad);
        const dy = Math.cos(angleRad);
        
        // Project all corners onto the gradient axis to find min/max
        const projections = [UL, UR, LL, LR].map(pt => pt.x * dx + pt.y * dy);
        const minProj = Math.min(...projections);
        const maxProj = Math.max(...projections);
        
        // Get bounding box for grid area
        const minX = Math.min(UL.x, UR.x, LL.x, LR.x);
        const maxX = Math.max(UL.x, UR.x, LL.x, LR.x);
        const minY = Math.min(UL.y, UR.y, LL.y, LR.y);
        const maxY = Math.max(UL.y, UR.y, LL.y, LR.y);
        
        colorMode(HSB, 360, 100, 100, 100);
        
        // For each pixel in the bounding box
        for (let y = Math.floor(minY); y <= Math.ceil(maxY); y++) {
            for (let x = Math.floor(minX); x <= Math.ceil(maxX); x++) {
                // Project (x, y) onto gradient axis
                let proj = x * dx + y * dy;
                let t = (proj - minProj) / (maxProj - minProj);
                
                let gradCol = this._interpolateHSB(t);
                stroke(gradCol);
                point(x, y);
            }
        }
    }

    /**
     * Draws the gradient within a rotated rectangle (box) on the given grid.
     * 
     * TWO-ANGLE INTERACTION:
     * This method uses BOTH angle properties:
     * - this.angleDeg: Direction gradient flows WITHIN the box
     * - this.boxAngleDeg (or parameter): Rotation of the ENTIRE box shape
     * 
     * The boxAngleDeg parameter defaults to this.boxAngleDeg, allowing you to:
     * 1. Set box rotation once in constructor, or
     * 2. Modify this.boxAngleDeg as a property, or
     * 3. Override with parameter for one-time use
     * 
     * VISUAL EXAMPLE:
     * If angleDeg=0 and boxAngleDeg=45:
     * - Gradient flows from top-to-bottom (vertical)
     * - But the entire box is tilted 45° like a diamond
     * 
     * If angleDeg=90 and boxAngleDeg=45:
     * - Gradient flows left-to-right (horizontal) within box
     * - Box itself is tilted 45°
     * 
     * @param {SWGrid} grid - The grid for user-to-screen mapping
     * @param {SWPoint} UL - Upper left corner in user coordinates (before rotation)
     * @param {SWPoint} LR - Lower right corner in user coordinates (before rotation)
     * @param {number} [boxAngleDeg=this.boxAngleDeg] - Box rotation angle in degrees (defaults to property value)
     */
    drawBoxOnGrid(grid, UL, LR, boxAngleDeg = this.boxAngleDeg) {
        // Get the four corners of the box in user coordinates
        const UR = new SWPoint(LR.x, UL.y);
        const LL = new SWPoint(UL.x, LR.y);
        
        // Convert to screen coordinates
        let corners = [UL, UR, LR, LL].map(pt => grid.userToScreen(pt.x, pt.y));
        
        // Center of the box (in screen coordinates)
        const cx = (corners[0].x + corners[2].x) / 2;
        const cy = (corners[0].y + corners[2].y) / 2;
        
        // Compute box width and height in screen space
        const boxW = Math.hypot(corners[1].x - corners[0].x, corners[1].y - corners[0].y);
        const boxH = Math.hypot(corners[3].x - corners[0].x, corners[3].y - corners[0].y);
        
        // Rotation angle in radians (screen space)
        const theta = (boxAngleDeg || 0) * Math.PI / 180;
        
        // Save current drawing state
        push();
        translate(cx, cy);
        rotate(theta);
        
        // Draw the gradient within the box bounds (per-pixel)
        colorMode(HSB, 360, 100, 100, 100);
        
        for (let y = -boxH / 2; y <= boxH / 2; y++) {
            for (let x = -boxW / 2; x <= boxW / 2; x++) {
                // Project (x, y) onto gradient axis (in box local coords)
                // For the box gradient, we use the gradient angle relative to the box
                const gradAngleRad = (this.angleDeg || 0) * Math.PI / 180;
                const gradDx = Math.sin(gradAngleRad);
                const gradDy = Math.cos(gradAngleRad);
                
                // Project point onto gradient axis
                let proj = x * gradDx + y * gradDy;
                
                // Find min/max projections for the box corners
                const boxCorners = [
                    {x: -boxW/2, y: -boxH/2},
                    {x: boxW/2, y: -boxH/2},
                    {x: -boxW/2, y: boxH/2},
                    {x: boxW/2, y: boxH/2}
                ];
                const boxProj = boxCorners.map(pt => pt.x * gradDx + pt.y * gradDy);
                const minBoxProj = Math.min(...boxProj);
                const maxBoxProj = Math.max(...boxProj);
                
                // Calculate t based on projection
                let t = (proj - minBoxProj) / (maxBoxProj - minBoxProj);
                
                let gradCol = this._interpolateHSB(t);
                stroke(gradCol);
                point(x, y);
            }
        }
        
        pop();
    }
}

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