/*
File: swColor.js
Date: 2026-01-30
Author: klp
Workspace: SWaveJS2026-01-30-StgC
Purpose: SWColor class and predefined colors for SketchWaveJS
Comment(s):

TODO:

=== Notes ===:
- SWColor class represents colors in HSB color space with alpha transparency.
- Includes methods for color manipulation (darken, brighten, saturate, 
    desaturate, change alpha).
- Predefined global SWColor instances for common colors (swRed, swBlue, swGreen, etc.).
- Initialization function to set up all predefined colors.
- Static method to create SWColor from RGB values.
- Static copy method to duplicate SWColor instances.
- Verification of color property ranges to ensure valid values.
- Script must be loaded before the p5js script, but colors are initialized in setup() after    colorMode is set.
- Code is only compatible with p5.js environment.
- Additional methods: createDarkerColor(factor), createLighterColor(factor) and createAdjustedSaturation(factor)

*/

console.log("[swColor.js] SWColor class and predefined colors loaded");

// Declare all SWColor variables (uninitialized)
var swBlack, swWhite, swOffWhite, swLightGray, swDarkGray;
var swRed, swDarkFlesh, swOrange, swLightFlesh, swBrown;
var swLightBrown, swTrunkBrown, swGold, swDarkYellow, swYellow, swDeepYellow, swPaleYellow;
var swMedGreen, swDarkGreen, swForestGreen, swGreen, swCyan;
var swLightishBlue, swDarkBlue, swDeepBlue, swBlue, swLightBlue;
var swNavy, swIndigo, swViolet, swDarkPurple, swMagenta, swPurple;
var swPink, swHotPink, swDeepPink, swMaroon;

function initializeSWColors() {
    console.log("...initializeSWColors..."); 
    //must be called AFTER colorMode is set in setup()
    // This function initializes all global SWColor instances
    
    // In order of ascending hue
    swBlack = new SWColor(0, 0, 0, 100, "black");
    swWhite = new SWColor(0, 0, 100, 100, "white");
    swOffWhite = new SWColor(0, 0, 93, 100, "offWhite");
    swLightGray = new SWColor(0, 0, 80, 100, "lightGray");
    swDarkGray = new SWColor(0, 0, 40, 100, "darkGray");
    swRed = new SWColor(0, 100, 100, 100, "red");
    swDarkFlesh = new SWColor(15, 46, 61, 100, "darkFlesh");
    swOrange = new SWColor(18, 80, 100, 100, "orange");
    swLightFlesh = new SWColor(18, 24, 74, 100, "lightFlesh");
    swBrown = new SWColor(27, 100, 65, 100, "brown");
    swLightBrown = new SWColor(30, 100, 80, 100, "lightBrown");
    swTrunkBrown = new SWColor(40, 86, 54, 100, "trunkBrown");
    swGold = new SWColor(45, 94, 92, 100, "gold");
    swDarkYellow = new SWColor(59, 100, 80, 100, "darkYellow");
    swYellow = new SWColor(60, 100, 100, 100, "yellow");
    swDeepYellow = new SWColor(60, 100, 59, 100, "deepYellow");
    swPaleYellow = new SWColor(60, 41, 100, 100, "paleYellow");
    swMedGreen = new SWColor(110, 36, 52, 100, "medGreen");
    swDarkGreen = new SWColor(133, 87, 40, 100, "darkGreen");
    swForestGreen = new SWColor(146, 100, 46, 100, "forestGreen");
    swGreen = new SWColor(120, 100, 100, 100, "green");
    swCyan = new SWColor(180, 100, 100, 100, "cyan");
    swLightishBlue = new SWColor(200, 60, 100, 100, "lightishBlue");
    swDarkBlue = new SWColor(204, 100, 100, 100, "darkBlue");
    swDeepBlue = new SWColor(228, 100, 100, 100, "deepBlue");
    swBlue = new SWColor(240, 100, 100, 100, "blue");
    swLightBlue = new SWColor(240, 22, 100, 100, "lightBlue");
    swNavy = new SWColor(240, 100, 50, 100, "navy");
    swIndigo = new SWColor(255, 100, 40, 100, "indigo");
    swViolet = new SWColor(282, 100, 27, 100, "violet");
    swDarkPurple = new SWColor(270, 100, 80, 100, "darkPurple");
    swMagenta = new SWColor(300, 100, 100, 100, "magenta");
    swPurple = new SWColor(300, 67, 60, 100, "purple");
    swPink = new SWColor(320, 25, 100, 100, "pink");
    swHotPink = new SWColor(324, 100, 100, 100, "hotPink");
    swDeepPink = new SWColor(334, 100, 79, 100, "deepPink");
    swMaroon = new SWColor(348, 100, 71, 100, "maroon");
}//end initializeSWColors

class SWColor {  
    constructor(h, s, b, a = 100, name = "") {
        //hue: 0-360, saturation: 0-100, brightness: 0-100, alpha: 0-100
        //wraparound hue to be within 0-360
        let anH = h;
        if(h < 0){
            anH = 360 + (h % 360);
        }else{
            anH = h % 360;  
        }
        if(anH !== h) console.log(`SWColor hue adjusted to ${anH}`);
        this.h = anH;
        this.s = s;
        this.b = b;
        this.a = a;
        this.name = name;

        //defensive programming: verify values are in range
        //hue is verified with wraparound above
        this.verify('s', 0, 100);
        this.verify('b', 0, 100);
        this.verify('a', 0, 100);

        // Create and store the p5.js color object
        //console.log(`SWColor created: ${this.toString()}`);
        this.col = color(this.h, this.s, this.b, this.a);
    }//end constructor

    // Copy constructor: returns a new SWColor with the same 
    // properties as the given SWColor instance
    static copy(other) {
        if (!(other instanceof SWColor)) {
            throw new Error('Argument to SWColor.copy must be an SWColor instance');
        }
        return new SWColor(other.h, other.s, other.b, other.a, other.name);
    }//end copy

    toString() {
        return `SWColor(name: '${this.name}', h: ${this.h}, s: ${this.s}, b: ${this.b}, a: ${this.a})`;
    }//end toString

    getHexStr() {
        // Converts this.col (p5.Color) to a CSS hex string
        // p5.js: red(), green(), blue() extract RGB from color object
        let r = Math.round(red(this.col));
        let g = Math.round(green(this.col));
        let b = Math.round(blue(this.col));
        return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
    }//end getHexStr

    getRGBStr() {
        // Returns a string in the format (r, g, b)
        // DWR! red, green, blue are p5.js functions!
        let r = red(this.col);
        let g = green(this.col);
        let b = blue(this.col);
        return `(${r}, ${g}, ${b})`;
    }//end getRGBStr

    verify(prop, min, max) {
        //ensures that the given property (h, s, b, a) is within the specified range
        if (this[prop] < min) {
            this[prop] = min;
        } else if (this[prop] > max) {
            this[prop] = max;
        }
    }//end verify

    darkenBy(factor) {
        // Decrease brightness by multiplying by (1 - factor), 
        // e.g., factor=0.2 darkens by 20%
        const originalB = this.b;
        this.b = originalB * (1 - factor);
        this.verify('b', 0, 100);
        this.col = color(this.h, this.s, this.b, this.a);
    }//end darkenBy

    brightenBy(factor) {
        // Increase brightness by multiplying by (1 + factor), 
        // e.g., factor=0.2 brightens by 20%
        const originalB = this.b;
        this.b = originalB * (1 + factor);
        this.verify('b', 0, 100);
        this.col = color(this.h, this.s, this.b, this.a);
    }//end brightenBy

    saturateBy(factor) {
        // Increase saturation by multiplying by (1 + factor), 
        // e.g., factor=0.2 increases saturation by 20%
        const originalS = this.s;
        this.s = originalS * (1 + factor);
        this.verify('s', 0, 100);
        this.col = color(this.h, this.s, this.b, this.a);
        return this;
    }//end saturateBy

    desaturateBy(factor) {
        // Decrease saturation by multiplying by (1 - factor), 
        // e.g., factor=0.2 decreases saturation by 20%
        const originalS = this.s;
        this.s = originalS * (1 - factor);
        this.verify('s', 0, 100);
        this.col = color(this.h, this.s, this.b, this.a);
        return this;
    }//end desaturateBy

    changeAlphaBy(factor) {
        // factor can be positive or negative
        // Change alpha by multiplying by (1 + factor), 
        // e.g., factor=0.2 increases alpha by 20%  
        const originalA = this.a;
        this.a = originalA * (1 + factor);
        this.verify('a', 0, 100);
        this.col = color(this.h, this.s, this.b, this.a);
        return this;
    }//end changeAlphaBy

    setAlphaTo(a) {
        this.a = a;
        this.verify('a', 0, 100);
        this.col = color(this.h, this.s, this.b, this.a);
        return this;
    }//end setAlphaTo

    // Static method: create SWColor from RGB values
    static fromRGB(r, g, b, a = 100, name = "") {
        // Convert RGB (0-255) to HSB (h: 0-360, s: 0-100, b: 0-100)
        r /= 255; g /= 255; b /= 255;
        let max = Math.max(r, g, b), min = Math.min(r, g, b);
        let h, s, v = max;
        let d = max - min;
        s = max === 0 ? 0 : d / max;
        if (max === min) {
            h = 0;
        } else {
            switch (max) {
                case r: h = ((g - b) / d) % 6; break;
                case g: h = ((b - r) / d) + 2; break;
                case b: h = ((r - g) / d) + 4; break;
            }
            h *= 60;
            if (h < 0) h += 360;
        }
        return new SWColor(Math.round(h), Math.round(s * 100), Math.round(v * 100), a, name);
    }//end fromRGB

    static fromHex(hex, alpha = 100, name = "") {
        // Ensure hex starts with '#'
        if (!hex.startsWith("#")) hex = "#" + hex;
        // Use p5.js color() to parse hex
        const c = color(hex);
        // Extract HSB values
        colorMode(HSB, 360, 100, 100, 100);
        const h = hue(c);
        const s = saturation(c);
        const br = brightness(c);
        return new SWColor(h, s, br, alpha, name);
    }//end fromHex

    cycleParameter(param, sinusoid) {
        // param: "h", "s", "b", or "a"
        // sinusoid: SWSinusoid object with a getValue() method
        if (!["h", "s", "b", "a"].includes(param)) {
            throw new Error("Invalid parameter for SWColor.cycleParameter");
        }
        this[param] = sinusoid.getValue();
        if (param === "h") {
            //wraparound hue to be within 0-360
            if(this.h < 0){
                this.h = 360 + (this.h % 360);
            }else{
                this.h = this.h % 360;  
            }
        }else{
            this.verify(param, 0, 100);
        }
        this.col = color(this.h, this.s, this.b, this.a);
    }//end cycleParameter

    /**
     * Returns a new SWColor with brightness scaled by the 
     * given factor (0-1 = darker, >1 = brighter)
     * @param {number} factor - The factor to scale brightness (b)
     * @returns {SWColor} New SWColor with adjusted brightness
     */
    createDarkerColor(factor) {
        // Clamp factor to non-negative
        factor = Math.max(0, factor);
        let newB = this.b * factor;
        // Clamp brightness to [0, 100]
        newB = Math.max(0, Math.min(100, newB));
        return new SWColor(this.h, this.s, newB, this.a, this.name ? this.name + '_darker' : undefined);
    }//end createDarkerColor

    /**
     * Returns a new SWColor with brightness increased by the given factor (>1 = lighter, 0-1 = dimmer)
     * @param {number} factor - The factor to scale brightness (b)
     * @returns {SWColor} New SWColor with adjusted brightness
     */
    createLighterColor(factor) {
        factor = Math.max(0, factor);
        let newB = this.b * factor;
        newB = Math.max(0, Math.min(100, newB));
        return new SWColor(this.h, this.s, newB, this.a, this.name ? this.name + '_lighter' : undefined);
    }//end createLighterColor

    /**
     * Returns a new SWColor with saturation scaled by the given factor
     * @param {number} factor - The factor to scale saturation (s)
     * @returns {SWColor} New SWColor with adjusted saturation
     */
    createAdjustedSaturation(factor) {
        factor = Math.max(0, factor);
        let newS = this.s * factor;
        newS = Math.max(0, Math.min(100, newS));
        return new SWColor(this.h, newS, this.b, this.a, this.name ? this.name + '_sat' : undefined);
    }//end createAdjustedSaturation


    /* 
    --- Notes ---
    'verify' method ensures color properties stay within valid ranges. It used to throw an error using code like this:
    if (this[prop] < min || this[prop] > max) {
        throw new Error(`${prop} value ${this[prop]} is out of range (${min}-${max})`);
    }   
    However, this could disrupt program flow if a color value was slightly out of bounds due to calculations.
    Instead, it now clamps the value to the nearest valid limit and logs a message. This makes the class more robust

    Regarding static copy method:
    A static copy method is used instead of a traditional 
    copy constructor because JavaScript does not support multiple constructors.
    Using a static method allows us to create a new instance based on an existing one without overloading the constructor.
    This approach is clear and idiomatic in JavaScript, providing a straightforward way to duplicate objects.

    The static keyword before copy means that copy is a static method of the SWColor class. This means you call it on the class itself (SWColor.copy(...)), not on an instance (not swRed.copy(...)).

    It is necessary if you want to use the method without needing an instance, and it makes sense here: copy creates a new SWColor from an existing one, but doesn’t depend on the state of a particular SWColor instance.

    If you removed static, you would have to call copy on an instance (e.g., swRed.copy(other)), which is less clear for a copy constructor pattern. So, static is appropriate and recommended in this case.
    */

}//end SWColor class




