/*
File: swPoint.js
Date: 2026-02-17
Author: klp
Workspace: SWLineDemo2026-02-12-Stg1
Purpose: SWLine class for SketchWaveJS
Comment(s):

An important update: If a 'null' color is passed for the strokeColor, the point will use a default color (which can be set when drawing) instead of no color. This allows for more flexible styling where the point can inherit a color from its context if not explicitly set.

TODO:

=== Notes ===:
- This class assumes p5.js and SWColor are loaded in the environment.
- SWPoint class represents points or vectors in 2D or 3D space with styling options.
- Includes properties for position (x, y, z), stroke weight, and stroke color.
- Supports pen trail functionality to track and draw the path of the point.
- Designed to be compatible with p5.js and the SWColor class.
- Methods for drawing the point and its trail in screen coordinates.
- Additional methods for movement and vector math can be added as needed.
- For heavy math, consider extending or wrapping p5.Vector.
- strokeColor should be an SWColor instance for consistency.
- Optionally, you can add fillColor or other style properties.
- Pen trail logic includes enabling/disabling the pen, storing trail points, and limiting trail length.
- You can extend this class for animated or interactive points.
*/

console.log("[swPoint.js] SWPoint class loaded.");

class SWPoint {
    /**
     * @param {number} x - X coordinate
     * @param {number} y - Y coordinate
     * @param {number} [z] - Z coordinate (optional, for 3D)
     * @param {number} [strokeWeight=1] - Stroke weight
     * @param {SWColor} [strokeColor] - Stroke color (SWColor instance)
     * @param {string} [label] - Label (single letter, optional)
     */
    
    constructor(x, y, z = undefined, strokeWeight = 1, strokeColor = undefined, label = undefined) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.strokeWeight = strokeWeight;
        this.strokeColor = strokeColor; // Always store SWColor instance
        this.label = label; // Label (typically a single letter)
        this.showLabel = false; // Whether to display the label
        this.labelOffset = { x: 10, y: -10 }; // Offset for label position
        this.labelFontSize = 12; // Font size for label text
        this.isDraggable = false; // Whether the point can be dragged

        // Pen trail logic
        this.penOn = false;
        this.trail = [];
        this.maxTrailLength = 500; // default, can be changed
    }//end constructor

    /**
     * Copy constructor: returns a new SWPoint with the same properties as the given SWPoint instance
     * Creates a deep copy of all properties including trail and labelOffset
     * @param {SWPoint} other - The SWPoint instance to copy
     * @returns {SWPoint} A new SWPoint with copied properties
     */
    static copy(other) {
        if (!(other instanceof SWPoint)) {
            throw new Error('Argument to SWPoint.copy must be an SWPoint instance');
        }
        // Create new point with basic properties
        const newPoint = new SWPoint(
            other.x, 
            other.y, 
            other.z, 
            other.strokeWeight, 
            other.strokeColor ? SWColor.copy(other.strokeColor) : undefined,
            other.label
        );
        
        // Copy additional properties
        newPoint.showLabel = other.showLabel;
        newPoint.labelOffset = { x: other.labelOffset.x, y: other.labelOffset.y };
        newPoint.labelFontSize = other.labelFontSize;
        newPoint.isDraggable = other.isDraggable;
        newPoint.penOn = other.penOn;
        newPoint.maxTrailLength = other.maxTrailLength;
        
        // Deep copy the trail array
        newPoint.trail = other.trail.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
        
        return newPoint;
    }//end copy

    /**
     * Draws the point using p5.js in screen coordinates
     * @param {SWColor} [defaultColor] - Color to use if strokeColor is null
     */
    draw(defaultColor = undefined) {
        // Use strokeColor if set, otherwise use defaultColor, otherwise swBlack
        let colorToUse = this.strokeColor;
        if (!colorToUse) {
            colorToUse = defaultColor || swBlack;
        }
        if (colorToUse && colorToUse.col) {
            stroke(colorToUse.col);
        }
        strokeWeight(this.strokeWeight);
        if (this.z !== undefined) {
            point(this.x, this.y, this.z);
        } else {
            point(this.x, this.y);
        }
        noStroke();
        strokeWeight(1);
        
        // Draw label if showLabel is true
        if (this.showLabel && this.label) {
            fill(colorToUse && colorToUse.col ? colorToUse.col : 0);
            textAlign(CENTER, CENTER);
            textSize(this.labelFontSize);
            text(this.label, this.x + this.labelOffset.x, this.y + this.labelOffset.y);
            noFill();
        }
    }//end draw

    /**
     * Draws the point using user (grid) coordinates mapped by the given SWGrid
     * @param {SWGrid} grid
     * @param {SWColor} [defaultColor] - Color to use if strokeColor is null
     */
    drawOnGrid(grid, defaultColor = undefined) {
        const {x: px, y: py} = grid.userToScreen(this.x, this.y);
        // Use strokeColor if set, otherwise use defaultColor, otherwise swBlack
        let colorToUse = this.strokeColor;
        if (!colorToUse) {
            colorToUse = defaultColor || swBlack;
        }
        if (colorToUse && colorToUse.col) {
            stroke(colorToUse.col);
        }
        strokeWeight(this.strokeWeight);
        if (this.z !== undefined) {
            point(px, py, this.z);
        } else {
            point(px, py);
        }
        noStroke();
        strokeWeight(1);
        
        // Draw label if showLabel is true
        if (this.showLabel && this.label) {
            fill(colorToUse && colorToUse.col ? colorToUse.col : 0);
            textAlign(CENTER, CENTER);
            textSize(this.labelFontSize);
            text(this.label, px + this.labelOffset.x, py + this.labelOffset.y);
            noFill();
        }
    }//end drawOnGrid

    /**
     * Pen trail enhancements:
     * - penOn: whether the pen is active
     * - trail: array of previous positions [{x, y, z}]
     * - maxTrailLength: maximum number of points in the trail
     */
    /**
     * Draws the pen trail in screen coordinates
     */
    drawTrail() {
        if (!this.penOn || this.trail.length < 2) return;
        noFill();
        stroke(this.strokeColor && this.strokeColor.col ? this.strokeColor.col : 0);
        strokeWeight(Math.max(1, this.strokeWeight / 2));
        beginShape();
        for (const pt of this.trail) {
            if (this.z !== undefined && pt.z !== undefined) {
                vertex(pt.x, pt.y, pt.z);
            } else {
                vertex(pt.x, pt.y);
            }
        }
        endShape();
        noStroke();
        strokeWeight(1);
    }//end drawTrail

    /**
     * Draws the pen trail in user (grid) coordinates
     * @param {SWGrid} grid
     */
    drawTrailOnGrid(grid) {
        if (!this.penOn || this.trail.length < 2) return;
        noFill();
        stroke(this.strokeColor && this.strokeColor.col ? this.strokeColor.col : 0);
        strokeWeight(Math.max(1, this.strokeWeight / 2));
        beginShape();
        for (const pt of this.trail) {
            const {x, y} = grid.userToScreen(pt.x, pt.y);
            vertex(x, y);
        }
        endShape();
        noStroke();
        strokeWeight(1);
    }//end drawTrailOnGrid

    /**
     * Moves the point by dx, dy, dz
     * If pen is on, records the new position in the trail
     */
    move(dx, dy, dz = 0) {
        this.x += dx;
        this.y += dy;
        if (this.z !== undefined) this.z += dz;
        this._updateTrail();
    }//end move

    /**
     * Call this after manually setting x/y/z to update the trail if pen is on
     * Private method: do not call directly (as indicated by underscore prefix)
     */
    _updateTrail() {
        if (this.penOn) {
            // Only add if position changed
            if (
                this.trail.length === 0 ||
                this.trail[this.trail.length - 1].x !== this.x ||
                this.trail[this.trail.length - 1].y !== this.y ||
                this.trail[this.trail.length - 1].z !== this.z
            ) {
                this.trail.push({ x: this.x, y: this.y, z: this.z });
                if (this.trail.length > this.maxTrailLength) {
                    this.trail.shift();
                }
            }
        }
    }//end _updateTrail

    /**
     * Enable or disable the pen trail
     * @param {boolean} on
     */
    setPen(on = true) {
        this.penOn = on;
        if (!on) this.trail = [];
    }//end setPen

    /**
     * Set the maximum trail length
     * @param {number} n
     */
    setMaxTrailLength(n) {
        this.maxTrailLength = n;
        if (this.trail.length > n) {
            this.trail = this.trail.slice(-n);
        }
    }//end setMaxTrailLength

    /**
     * Clears the pen trail history, regardless of pen state
     */
    clearTrail() {
        this.trail = [];
    }//end clearTrail

    /**
     * Sets the stroke color
     * @param {SWColor} swColor
     */
    setStrokeColor(swColor) {
        this.strokeColor = swColor;
    }//end setStrokeColor

    /**
     * Sets the stroke weight
     * @param {number} w
     */
    setStrokeWeight(w) {
        this.strokeWeight = w;
    }//end setStrokeWeight

    /**
     * Sets whether to show the label
     * @param {boolean} show
     */
    setShowLabel(show = true) {
        this.showLabel = show;
    }//end setShowLabel

    /**
     * Sets the label (typically a single letter)
     * @param {string} label
     */
    setLabel(label) {
        this.label = label;
    }//end setLabel

    /**
     * Sets the label font size
     * @param {number} size - Font size in pixels
     */
    setLabelFontSize(size) {
        this.labelFontSize = size;
    }//end setLabelFontSize

    /**
     * Sets the label offset based on a line slope to position the label perpendicular to the line
     * This prevents labels from being obscured by the line itself.
     * @param {number} slope - The slope of the line (use Infinity for vertical lines, 0 for horizontal)
     * @param {number} [distance=15] - Distance to offset the label from the point
     */
    setLabelOffsetFromSlope(slope, distance = 15) {
        if (slope === Infinity || slope === -Infinity) {
            // Vertical line: offset horizontally to the right
            this.labelOffset = { x: distance, y: 0 };
        } else if (slope === 0) {
            // Horizontal line: offset vertically upward
            this.labelOffset = { x: 0, y: -distance };
        } else {
            // General case: offset perpendicular to the line
            // If line has slope m, perpendicular direction vector is (-m, 1)
            const perp_x = -slope;
            const perp_y = 1;
            const length = Math.sqrt(perp_x * perp_x + perp_y * perp_y);
            const unit_x = perp_x / length;
            const unit_y = perp_y / length;
            this.labelOffset = { x: unit_x * distance, y: unit_y * distance };
        }
    }//end setLabelOffsetFromSlope

    /**
     * Manually sets the label position using explicit x,y offsets
     * Use this to override automatic positioning when labels are obscured by artwork
     * @param {number} offsetX - Horizontal offset from point (pixels)
     * @param {number} offsetY - Vertical offset from point (pixels, negative is up)
     */
    setLabelPosition(offsetX, offsetY) {
        this.labelOffset = { x: offsetX, y: offsetY };
    }//end setLabelPosition

    /**
     * Nudges the label by a small amount in either direction
     * Useful for fine-tuning label position or avoiding overlaps
     * @param {number} dx - Amount to move horizontally (pixels)
     * @param {number} dy - Amount to move vertically (pixels)
     */
    nudgeLabel(dx, dy) {
        this.labelOffset.x += dx;
        this.labelOffset.y += dy;
    }//end nudgeLabel

    /**
     * Sets label to common position above the point
     * @param {number} [distance=15] - Distance above the point
     */
    setLabelAbove(distance = 15) {
        this.labelOffset = { x: 0, y: -distance };
    }//end setLabelAbove

    /**
     * Sets label to common position below the point
     * @param {number} [distance=15] - Distance below the point
     */
    setLabelBelow(distance = 15) {
        this.labelOffset = { x: 0, y: distance };
    }//end setLabelBelow

    /**
     * Sets label to common position to the left of the point
     * @param {number} [distance=15] - Distance to the left
     */
    setLabelLeft(distance = 15) {
        this.labelOffset = { x: -distance, y: 0 };
    }//end setLabelLeft

    /**
     * Sets label to common position to the right of the point
     * @param {number} [distance=15] - Distance to the right
     */
    setLabelRight(distance = 15) {
        this.labelOffset = { x: distance, y: 0 };
    }//end setLabelRight

    /**
     * Automatically adjusts label position if it goes off the canvas boundaries
     * Checks all four edges and repositions the label to keep it visible
     * @param {SWGrid} grid - The grid for boundary checking
     * @param {number} [margin=5] - Minimum distance from edge in screen pixels
     */
    labelProximityCheck(grid, margin = 5) {
        if (!this.label || !this.showLabel) return; // No label to adjust
        
        // Convert point to screen coordinates
        const {x: px, y: py} = grid.userToScreen(this.x, this.y);
        
        // Calculate label position in screen coordinates
        const labelX = px + this.labelOffset.x;
        const labelY = py + this.labelOffset.y;
        
        // Get canvas boundaries (assuming grid knows about screen bounds)
        const canvasWidth = grid.screenWidth || width;
        const canvasHeight = grid.screenHeight || height;
        
        // Approximate text width/height for better boundary detection
        const textWidth = 15; // Approximate width for single character
        const textHeight = 12; // Text size
        
        let adjustedOffsetX = this.labelOffset.x;
        let adjustedOffsetY = this.labelOffset.y;
        
        // Check right boundary - if label goes off right, move to left
        if (labelX + textWidth/2 > canvasWidth - margin) {
            adjustedOffsetX = -15; // Move to left of point
        }
        
        // Check left boundary - if label goes off left, move to right
        if (labelX - textWidth/2 < margin) {
            adjustedOffsetX = 15; // Move to right of point
        }
        
        // Check top boundary - if label goes off top, move below
        if (labelY - textHeight/2 < margin) {
            adjustedOffsetY = 15; // Move below point
        }
        
        // Check bottom boundary - if label goes off bottom, move above
        if (labelY + textHeight/2 > canvasHeight - margin) {
            adjustedOffsetY = -15; // Move above point
        }
        
        // Apply adjusted offset
        this.labelOffset = { x: adjustedOffsetX, y: adjustedOffsetY };
    }//end labelProximityCheck

    /**
     * Returns a string representation
     */
    logAllInfo() {
        return `SWPoint(x: ${this.x}, y: ${this.y}` +
            (this.z !== undefined ? `, z: ${this.z}` : '') +
            `, strokeWeight: ${this.strokeWeight}, strokeColor: ${this.strokeColor ? this.strokeColor.toString() : 'none'}, label: ${this.label || 'none'}, showLabel: ${this.showLabel}, penOn: ${this.penOn}, trailLen: ${this.trail.length})`;
    }//end logAllInfo

    /**
     * Returns a simple string representation of the point
     * Format: "Pt.A @ (x, y), colorName" if labeled, or "Pt @ (x, y), colorName" if unlabeled
     * @returns {string}
     */
    toString() {
        const labelPart = this.label ? `.${this.label}` : '';
        const x = this.x.toFixed(2);
        const y = this.y.toFixed(2);
        const coords = this.z !== undefined ? `(${x}, ${y}, ${this.z.toFixed(2)})` : `(${x}, ${y})`;
        const colorName = this.strokeColor && this.strokeColor.name ? this.strokeColor.name : 'no color';
        return `Pt${labelPart} @ ${coords}, ${colorName}`;
    }//end toString

    /**
     * Returns the Euclidean distance to another SWPoint in user coordinates
     * @param {SWPoint} otherSWPt
     * @returns {number}
     */
    distanceTo(otherSWPt) {
        const dx = this.x - otherSWPt.x;
        const dy = this.y - otherSWPt.y;
        // If both have z, use 3D distance; otherwise, 2D
        if (this.z !== undefined && otherSWPt.z !== undefined) {
            const dz = this.z - otherSWPt.z;
            return Math.sqrt(dx * dx + dy * dy + dz * dz);
        } else {
            return Math.sqrt(dx * dx + dy * dy);
        }
    }//end distanceTo

    /**
     * Checks if a screen coordinate is within the point's hit area
     * Used for drag detection and mouse interaction
     * @param {number} screenX - Mouse x in screen coordinates
     * @param {number} screenY - Mouse y in screen coordinates
     * @param {SWGrid} grid - The grid for coordinate conversion
     * @param {number} [tolerance=10] - Hit detection radius in screen pixels
     * @returns {boolean} True if the coordinate is within tolerance of the point
     */
    containsPoint(screenX, screenY, grid, tolerance = 10) {
        const {x: px, y: py} = grid.userToScreen(this.x, this.y);
        return dist(screenX, screenY, px, py) <= tolerance;
    }//end containsPoint

    /**
     * Sets the point to a new position in user coordinates
     * @param {number} x - New x coordinate (user coordinates)
     * @param {number} y - New y coordinate (user coordinates)
     */
    setPosition(x, y) {
        this.x = x;
        this.y = y;
        this._updateTrail(); // Update trail if pen is on
    }//end setPosition

    /**
     * Enables or disables draggability for this point
     * @param {boolean} draggable
     */
    setDraggable(draggable = true) {
        this.isDraggable = draggable;
    }//end setDraggable
}//end SWPoint class
