/*global String*/
/*global atob*/
String.prototype.startsWith = (String.prototype.startsWith || function(needle)   { return (this.indexOf(needle) === 0); });
String.prototype.padStart   = (String.prototype.padStart   || function(len, pad) { let str = this; while(str.length < len) { str = pad + str; }; return str; });


/*
    Code golfing:
    This is a compacted list of all 148 CSS color names (from https://github.com/bahamas10/css-color-names/blob/master/css-color-names.json).
    Every seven characters in this array is a name and its RGB value.

        //3 chars name hash (rudimentary, but just enough to separate all unique colors),
        //based on https://stackoverflow.com/a/15710692/1869660
        hash = [].reduce.call(name.replace('ey','ay'), (h, c) => (h << 2) + c.charCodeAt(0), 0)
                            .toString(36).slice(-3);

        //4 chars base64 color value. Split the hex into [R, G, B], cast to bytes and base64-encode the byte array:
        hex64 = btoa( hex.match(/../g).map(x => String.fromCharCode(parseInt(x, 16))).join('') );

const colorNames = '735AACA770//Xub218Pj/mo5+uvX6mdAP//gtpf//Ur258P//q1d9fXcxop/+TEq9zAAAAqfg/+vN6m1AAD/ngoiiviqt6pSoqzyo3riHxvdX56grk1f/8Aax10mkeqts/39QxbtZJXttkb//jcyxm3BQ86rmAP//wl5AACLwqqAIuL3y8uIYLwv1qampniqAGQAns5vbdrmohiwCLw5uVWsvsdd/4wAsegmTLMqagiwAAsqi6ZZ6uz6j7yPxtzSD2Lxk3L09PudbAM7RwsolADT0kz/xSTfuhAL//vfhaWlpyuxHpD/43rsiIiwn9//rw39uIosi9bp/wD/6w73Nzc9s5+Pj/6v8/9cA3b42qUg6vxgICArmaAIAAtdfrf8vf9n8P/wek3/2m0xnczVxc3bvSwCCsdt///wrvp8OaMs5i5ub6iyk//D1e8ifPwAoui//rNpyxrdjmw9c8ICAq4i4P//mx9+vrSq8t09PTx1ukO6Qqlv/7bBuuy/6B690uILKqpfdh876sd9d4iZnehsMTe0dv///g71lAP8A4nmMs0ys9u+vDmg9d/wD/4pmgAAAcurZs2qzllAADN4lkulXT6txk3Db66qPLNxozre2juokuAPqalj3SNHMgdkxxWF60pGRlwxfl9f/6hr5/+Thx6q/+S1m85/96tutd/fXmszxgIAAe4ma44j8rl/6UAmu0/0UA8so2nDWji87uiqumqmPuY9xbr+7u4rs23CTsb8/+/V95a/9q577xzYU/78z/8DL7b53aDdsu1sODmb11gACAy5nZjOZ1so/wAAlvevI+Pn09QWnhm7ui0UT94q+oBy7ei9KRg5aqLotXad5oFItasmwMDAaihh87r9fdalrN9p9cICQ7gz//r6k5uAP9/4qhRoK01te0rSM7cwAICA91x2L/Yclr/2NHcw1QODQd6w7oLuua09d6zudh////t359fX1enn//8Ao0ims0y';
let  colorNamesDeser;
*/

//https://github.com/bahamas10/css-color-names/blob/master/css-color-names.json
//  const colorNames = { aliceblue:'#f0f8ff', antiquewhite:'#faebd7', aqua:'#00ffff', aquamarine:'#7fffd4', azure:'#f0ffff', beige:'#f5f5dc', bisque:'#ffe4c4', black:'#000000', blanchedalmond:'#ffebcd', blue:'#0000ff', blueviolet:'#8a2be2', brown:'#a52a2a', burlywood:'#deb887', cadetblue:'#5f9ea0', chartreuse:'#7fff00', chocolate:'#d2691e', coral:'#ff7f50', cornflowerblue:'#6495ed', cornsilk:'#fff8dc', crimson:'#dc143c', cyan:'#00ffff', darkblue:'#00008b', darkcyan:'#008b8b', darkgoldenrod:'#b8860b', darkgray:'#a9a9a9', darkgreen:'#006400', darkgrey:'#a9a9a9', darkkhaki:'#bdb76b', darkmagenta:'#8b008b', darkolivegreen:'#556b2f', darkorange:'#ff8c00', darkorchid:'#9932cc', darkred:'#8b0000', darksalmon:'#e9967a', darkseagreen:'#8fbc8f', darkslateblue:'#483d8b', darkslategray:'#2f4f4f', darkslategrey:'#2f4f4f', darkturquoise:'#00ced1', darkviolet:'#9400d3', deeppink:'#ff1493', deepskyblue:'#00bfff', dimgray:'#696969', dimgrey:'#696969', dodgerblue:'#1e90ff', firebrick:'#b22222', floralwhite:'#fffaf0', forestgreen:'#228b22', fuchsia:'#ff00ff', gainsboro:'#dcdcdc', ghostwhite:'#f8f8ff', gold:'#ffd700', goldenrod:'#daa520', gray:'#808080', green:'#008000', greenyellow:'#adff2f', grey:'#808080', honeydew:'#f0fff0', hotpink:'#ff69b4', indianred:'#cd5c5c', indigo:'#4b0082', ivory:'#fffff0', khaki:'#f0e68c', lavender:'#e6e6fa', lavenderblush:'#fff0f5', lawngreen:'#7cfc00', lemonchiffon:'#fffacd', lightblue:'#add8e6', lightcoral:'#f08080', lightcyan:'#e0ffff', lightgoldenrodyellow:'#fafad2', lightgray:'#d3d3d3', lightgreen:'#90ee90', lightgrey:'#d3d3d3', lightpink:'#ffb6c1', lightsalmon:'#ffa07a', lightseagreen:'#20b2aa', lightskyblue:'#87cefa', lightslategray:'#778899', lightslategrey:'#778899', lightsteelblue:'#b0c4de', lightyellow:'#ffffe0', lime:'#00ff00', limegreen:'#32cd32', linen:'#faf0e6', magenta:'#ff00ff', maroon:'#800000', mediumaquamarine:'#66cdaa', mediumblue:'#0000cd', mediumorchid:'#ba55d3', mediumpurple:'#9370db', mediumseagreen:'#3cb371', mediumslateblue:'#7b68ee', mediumspringgreen:'#00fa9a', mediumturquoise:'#48d1cc', mediumvioletred:'#c71585', midnightblue:'#191970', mintcream:'#f5fffa', mistyrose:'#ffe4e1', moccasin:'#ffe4b5', navajowhite:'#ffdead', navy:'#000080', oldlace:'#fdf5e6', olive:'#808000', olivedrab:'#6b8e23', orange:'#ffa500', orangered:'#ff4500', orchid:'#da70d6', palegoldenrod:'#eee8aa', palegreen:'#98fb98', paleturquoise:'#afeeee', palevioletred:'#db7093', papayawhip:'#ffefd5', peachpuff:'#ffdab9', peru:'#cd853f', pink:'#ffc0cb', plum:'#dda0dd', powderblue:'#b0e0e6', purple:'#800080', rebeccapurple:'#663399', red:'#ff0000', rosybrown:'#bc8f8f', royalblue:'#4169e1', saddlebrown:'#8b4513', salmon:'#fa8072', sandybrown:'#f4a460', seagreen:'#2e8b57', seashell:'#fff5ee', sienna:'#a0522d', silver:'#c0c0c0', skyblue:'#87ceeb', slateblue:'#6a5acd', slategray:'#708090', slategrey:'#708090', snow:'#fffafa', springgreen:'#00ff7f', steelblue:'#4682b4', tan:'#d2b48c', teal:'#008080', thistle:'#d8bfd8', tomato:'#ff6347', turquoise:'#40e0d0', violet:'#ee82ee', wheat:'#f5deb3', white:'#ffffff', whitesmoke:'#f5f5f5', yellow:'#ffff00', yellowgreen:'#9acd32' };
//
//More golfing, just because.. Simple hashing of the names - see nameToRgb()
const colorNames = {cb:'0f8ff',tqw:'aebd7',q:'-ffff',qmrn:'7fffd4',zr:'0ffff',bg:'5f5dc',bsq:'e4c4',bck:'---',nch:'ebcd',b:'--ff',bvt:'8a2be2',brwn:'a52a2a',brw:'deb887',ctb:'5f9ea0',hrt:'7fff-',chcT:'d2691e',cr:'7f50',rnw:'6495ed',crns:'8dc',crms:'dc143c',cn:'-ffff',Db:'--8b',Dcn:'-8b8b',Dgnr:'b8860b',Dgr:'a9a9a9',Dgrn:'-64-',Dkhk:'bdb76b',Dmgn:'8b-8b',Dvgr:'556b2f',Drng:'8c-',Drch:'9932cc',Dr:'8b--',Dsmn:'e9967a',Dsgr:'8fbc8f',DsTb:'483d8b',DsTg:'2f4f4f',Dtrq:'-ced1',Dvt:'94-d3',ppnk:'1493',pskb:'-bfff',mgr:'696969',grb:'1e90ff',rbrc:'b22222',rwht:'af0',stg:'228b22',chs:'-ff',gnsb:'dcdcdc',st:'8f8ff',g:'d7-',gnr:'daa520',gr:'808080',grn:'-8-0',grnw:'adff2f',hnw:'0fff0',htpn:'69b4',nnr:'cd5c5c',ng:'4b-82',vr:'0',khk:'0e68c',vnr:'e6e6fa',nrb:'0f5',wngr:'7cfc-',mnch:'acd',Lb:'add8e6',Lcr:'08080',Lcn:'e0ffff',Lgnr:'afad2',Lgr:'d3d3d3',Lgrn:'90ee90',Lpnk:'b6c1',Lsmn:'a07a',Lsgr:'20b2aa',Lskb:'87cefa',LsTg:'778899',Lstb:'b0c4de',Lw:'e0',m:'-ff-',mgrn:'32cd32',nn:'af0e6',mgnt:'-ff',mrn:'8--0',mqm:'66cdaa',mmb:'--cd',mmrc:'ba55d3',mmpr:'9370db',msg:'3cb371',mmsT:'7b68ee','':'-fa9a',mtr:'48d1cc',mmvt:'c71585',mnLb:'191970',ntc:'5fffa',mstr:'e4e1',mccs:'e4b5',vjw:'dead',nv:'--80',c:'df5e6',v:'808-0',vrb:'6b8e23',rng:'a5-',rngr:'45-',rch:'da70d6',pgnr:'eee8aa',pgrn:'98fb98',ptrq:'afeeee',pvtr:'db7093',ppwh:'efd5',pchp:'dab9',pr:'cd853f',pnk:'c0cb',pm:'dda0dd',pwrb:'b0e0e6',prp:'8-080',cc:'663399',r:'--',sbr:'bc8f8f',rb:'4169e1',sbrw:'8b4513',smn:'a8072',nbr:'4a460',sgrn:'2e8b57',ssh:'5ee',snn:'a0522d',svr:'c0c0c0',skb:'87ceeb',sTb:'6a5acd',sTgr:'708090',snw:'afa',n:'-ff7f',stb:'4682b4',tn:'d2b48c',t:'-8080',thst:'d8bfd8',tmT:'6347',trqs:'40e0d0',vt:'ee82ee',whT:'5deb3',wht:'',hts:'5f5f5',w:'-',wgrn:'9acd32'};


function printNum(num, decs = 1) {
    const str = (decs > 0) ? num.toFixed(decs).replace(/0+$/, '').replace(/\.$/, '')
                           : num.toString();
    return str || '0';
}


class Color {

    constructor(r, g, b, a) {

        const that = this;
        function parseString(input) {

            //HSL string. Examples:
            //	hsl(120, 60%,  50%) or
            //	hsla(240, 100%, 50%, .7)
            if( input.startsWith('hsl') ) {
                let [h, s, l, a] = input.match(/([\-\d\.e]+)/g).map(Number);
                if(a === undefined) { a = 1; }

                h /= 360;
                s /= 100;
                l /= 100;
                that.hsla = [h, s, l, a];
            }

            //RGB string. Examples:
            //	rgb(51, 170, 51)
            //	rgba(51, 170, 51, .7)
            else if( input.startsWith('rgb') ) {
                let [r, g, b, a] = input.match(/([\-\d\.e]+)/g).map(Number);
                if(a === undefined) { a = 1; }

                that.rgba = [r, g, b, a];
            }

            //Hex string or color name:
            else {
                if( input.startsWith('#') ) {
                    that.rgba = Color.hexToRgb(input);
                }
                else {
                    that.rgba = Color.nameToRgb(input) || Color.hexToRgb(input);
                }
            }
        }


        if( r === undefined ) {
            //No color input - the color can be set later through .hsla/.rgba/.hex
        }

        //Single input - RGB(A) array
        else if( Array.isArray(r) ) {
            this.rgba = r;
        }

        //Single input - CSS string
        else if( b === undefined ) {
            const color = r && ('' + r);
            if(color) {
                parseString(color.toLowerCase());
            }
        }

        else {
            this.rgba = [r, g, b, (a === undefined) ? 1 : a];
        }
    }


    /* RGBA representation */

    get rgba() {
        if(this._rgba) { return this._rgba; }
        if(!this._hsla) { throw new Error('No color is set'); }
        
        return (this._rgba = Color.hslToRgb(this._hsla));
    }
    set rgba(rgb) {
        if(rgb.length === 3) { rgb[3] = 1; }
        
        this._rgba = rgb;
        this._hsla = null;
    }

    printRGB(alpha) {
        const rgb = alpha ? this.rgba : this.rgba.slice(0, 3),
              vals = rgb.map((x, i) => printNum(x, (i === 3) ? 3 : 0));

        return alpha ? `rgba(${ vals })` : `rgb(${ vals })`;
    }
    get rgbString()  { return this.printRGB(); }
    get rgbaString() { return this.printRGB(true); }


    /* HSLA representation */

    get hsla() {
        if(this._hsla) { return this._hsla; }
        if(!this._rgba) { throw new Error('No color is set'); }
        
        return (this._hsla = Color.rgbToHsl(this._rgba));
    }
    set hsla(hsl) {
        if(hsl.length === 3) { hsl[3] = 1; }
        
        this._hsla = hsl;
        this._rgba = null;
    }

    printHSL(alpha) {
        const mults = [360, 100, 100, 1],
              suff =  ['', '%', '%', ''];

        const hsl = alpha ? this.hsla : this.hsla.slice(0, 3),
              //in printNum(), use enough decimals to represent all RGB colors:
              //https://gist.github.com/mjackson/5311256#gistcomment-2336011
              vals = hsl.map((x, i) => printNum(x * mults[i], (i === 3) ? 3 : 1) + suff[i]);
        
        return alpha ? `hsla(${ vals })` : `hsl(${ vals })`;
    }
    get hslString()  { return this.printHSL(); }
    get hslaString() { return this.printHSL(true); }


    /* HEX representation */

    get hex() {
        const rgb = this.rgba,
              hex = rgb.map((x, i) => (i < 3) ? x.toString(16)
                                              : Math.round(x * 255).toString(16));

        return '#' + hex.map(x => x.padStart(2, '0')).join('');
    }
    set hex(hex) {
        this.rgba = Color.hexToRgb(hex);
    }

    printHex(alpha) {
        const hex = this.hex;
        return alpha ? hex : hex.substring(0, 7);
    }


    /* Conversion utils */


    /**
     * Splits a HEX string into its RGB(A) components
     */
    static hexToRgb(input) {
        //Normalize all hex codes (3/4/6/8 digits) to 8 digits RGBA
        const hex = (input.startsWith('#') ? input.slice(1) : input)
            .replace(/^(\w{3})$/,          '$1F')                   //987      -> 987F
            .replace(/^(\w)(\w)(\w)(\w)$/, '$1$1$2$2$3$3$4$4')      //9876     -> 99887766
            .replace(/^(\w{6})$/,          '$1FF');                 //987654   -> 987654FF

        if(!hex.match(/^([0-9a-fA-F]{8})$/)) { throw new Error('Unknown hex color; ' + input); }

        const rgba = hex
            .match(/^(\w\w)(\w\w)(\w\w)(\w\w)$/).slice(1)  //98765432 -> 98 76 54 32
            .map(x => parseInt(x, 16));                    //Hex to decimal

        rgba[3] = rgba[3]/255;
        return rgba;
    }


    /**
     * Gets the RGB value from a CSS color name
     */
    static nameToRgb(input) {
        /* See comments on colorNames

        if(!colorNamesDeser) {
            colorNamesDeser = {};
            colorNames.match(/.{7}/g).forEach(x =>
                colorNamesDeser[x.slice(0, 3)] = atob(x.slice(-4)).split('').map(b => b.charCodeAt(0))
            );
        }
        const hash = [].reduce.call(input.replace('ey', 'ay'), (h, c) => (h << 2) + c.charCodeAt(0), 0)
                                .toString(36).slice(-3);

        return colorNamesDeser[hash];
        */

        //const hex = colorNames[input];
        //if(hex) {
        //    return Color.hexToRgb(hex);
        //}

        const hash = input.toLowerCase()
                          .replace('at', 'T')
                          .replace(/[aeiouyldf]/g, '')
                          .replace('ght', 'L')
                          .replace('rk', 'D')
                          .slice(-5, 4),
              hex = colorNames[hash];
        return (hex === undefined) ? hex
                                   : Color.hexToRgb(hex.replace(/\-/g, '00').padStart(6, 'f'));
    }


    /**
     * https://gist.github.com/mjackson/5311256
     * 
     * Converts an RGB color value to HSL. Conversion formula
     * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
     * Assumes r, g, and b are contained in the set [0, 255] and
     * returns h, s, and l in the set [0, 1].
     */
    static rgbToHsl([r, g, b, a]) {

        r /= 255;
        g /= 255;
        b /= 255;

        const max = Math.max(r, g, b),
              min = Math.min(r, g, b);
        let h,
            s,
            l = (max + min) / 2;

        if(max === min){
            h = s = 0; // achromatic
        }
        else {
            const d = max - min;
            s = (l > 0.5) ? d / (2 - max - min)
                          : d / (max + min);
            switch(max) {
                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                case g: h = (b - r) / d + 2; break;
                case b: h = (r - g) / d + 4; break;
            }
            
            h /= 6;
        }
        
        return [h, s, l, a];
    }


    /**
     * https://gist.github.com/mjackson/5311256
     * 
     * Converts an HSL color value to RGB. Conversion formula
     * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
     * Assumes h, s, and l are contained in the set [0, 1] and
     * returns r, g, and b in the set [0, 255].
     */
    static hslToRgb([h, s, l, a]) {

        let r, g, b;
        
        if (s === 0) {
            r = g = b = l; // achromatic
        }
        else {
            const hue2rgb = function(p, q, t) {
                if (t < 0) t += 1;
                if (t > 1) t -= 1;
                if (t < 1/6) return p + (q - p) * 6 * t;
                if (t < 1/2) return q;
                if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
                return p;
            };

            const q = (l < 0.5) ? l * (1 + s)
                                : l + s - (l * s),
                  p = (2 * l) - q;

            r = hue2rgb(p, q, h + 1/3);
            g = hue2rgb(p, q, h);
            b = hue2rgb(p, q, h - 1/3);
        }
        
        const rgba = [r * 255, g * 255, b * 255].map(Math.round);
        rgba[3] = a;
        
        return rgba;
    }

}


export default Color;
