/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @fileOverview Defines te {@link CKEDITOR.tools.color} class for handling different color formats.
 */

( function() {
	'use strict';

	/**
	 * Class representing a color. Provides conversion between various color formats:
	 *
	 * * Named colors.
	 * * Hexadecimal format (with alpha support).
	 * * RGB and RGBA formats.
	 * * HSL and HSLA formats.
	 *
	 * It can be used to validate and convert color between above formats.
	 *
	 * ```javascript
	 * var color = new CKEDITOR.tools.color( 'rgb( 225, 225, 225 )' );
	 * console.log( color.getHex() ); // #FFFFFF
	 *
	 * var color = new CKEDITOR.tools.color( 'red' );
	 * console.log( color.getHexWithAlpha() ); // #FF0000FF
	 * ```
	 * @since 4.16.0
	 * @class
	 */
	CKEDITOR.tools.color = CKEDITOR.tools.createClass( {

		/**
		 * Creates CKEDITOR.tools.color class instance.
		 *
		 * @constructor
		 * @param {String} colorCode
		 * @param {*} defaultValue Value which will be returned by any getter if passed color code is not valid.
		 */
		$: function( colorCode, defaultValue ) {
			this._.initialColorCode = colorCode;
			this._.defaultValue = defaultValue;

			this._.parseInput( colorCode );
		},

		proto: {
			/**
			 * Gets hexadecimal color representation.
			 *
			 * @returns {String/*} Hexadecimal color code (e.g. `#FF00FF`) or default value.
			 */
			getHex: function() {
				if ( !this._.isValidColor ) {
					return this._.defaultValue;
				}

				var color = this._.blendAlphaColor( this._.red, this._.green, this._.blue, this._.alpha );

				return this._.formatHexString( color[ 0 ], color[ 1 ], color[ 2 ] );
			},

			/**
			 * Gets hexadecimal color representation with separate alpha channel.
			 *
			 * @returns {String/*} Hexadecimal color code (e.g. `#FF00FF00`) or default value.
			 */
			getHexWithAlpha: function() {
				if ( !this._.isValidColor ) {
					return this._.defaultValue;
				}

				var alpha = Math.round( this._.alpha * CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE );

				return this._.formatHexString( this._.red, this._.green, this._.blue, alpha ) ;
			},

			/**
			 * Gets RGB color representation.
			 *
			 * @returns {String/*} RGB color representation (e.g. `rgb(255,255,255)`) or default value.
			 */
			getRgb: function() {
				if ( !this._.isValidColor ) {
					return this._.defaultValue;
				}

				var color = this._.blendAlphaColor( this._.red, this._.green, this._.blue, this._.alpha );

				return this._.formatRgbString( 'rgb', color[ 0 ], color[ 1 ], color[ 2 ]  );
			},

			/**
			 * Gets RGBA color representation.
			 *
			 * @returns {String/*} RGBA color representation (e.g. `rgba(255,255,255,0)`) or default value.
			 */
			getRgba: function() {
				if ( !this._.isValidColor ) {
					return this._.defaultValue;
				}

				return this._.formatRgbString( 'rgba', this._.red, this._.green, this._.blue, this._.alpha );
			},

			/**
			 * Gets HSL color representation.
			 *
			 * @returns {String/*} HSL color representation (e.g. `hsl(360,100%,50%)`) or default value.
			 *
			 */
			getHsl: function() {
				var isIntegerAlpha = this._.alpha === 0 || this._.alpha === 1,
					hsl,
					color;

				if ( !this._.isValidColor ) {
					return this._.defaultValue;
				}

				if ( this._.type === CKEDITOR.tools.color.TYPE_HSL && isIntegerAlpha ) {
					hsl = [ this._.hue, this._.saturation, this._.lightness ];
				} else {
					color = this._.blendAlphaColor( this._.red, this._.green, this._.blue, this._.alpha );
					hsl = this._.rgbToHsl( color[ 0 ], color[ 1 ], color[ 2 ] );
				}

				return this._.formatHslString( 'hsl', hsl[ 0 ], hsl[ 1 ], hsl[ 2 ] );
			},

			/**
			 * Gets HSLA color representation.
			 *
			 * @returns {String/*} HSLA color representation (e.g. `hsla(360,100%,50%,0)`) or default value.
			 */
			getHsla: function() {
				var hsl;

				if ( !this._.isValidColor ) {
					return this._.defaultValue;
				}

				if ( this._.type === CKEDITOR.tools.color.TYPE_HSL ) {
					hsl = [ this._.hue, this._.saturation, this._.lightness ];
				} else {
					hsl = this._.rgbToHsl( this._.red, this._.green, this._.blue );
				}

				return this._.formatHslString( 'hsla', hsl[ 0 ], hsl[ 1 ], hsl[ 2 ], this._.alpha );
			},

			/**
			 * Gets raw value passed to the constructor during color object creation.
			 *
			 * @returns {String} Raw value passed during color object creation.
			 */
			getInitialValue: function() {
				return this._.initialColorCode;
			}
		},

		_: {
			/**
			 * Initial color code provided to object constructor.
			 *
			 * @private
			 * @property {String}
			 */
			initialColorCode: '',

			/**
			 * Whether valid color input was passed.
			 *
			 * @private
			 * @property {Boolean}
			 */
			isValidColor: true,

			/**
			 * Original color type.
			 *
			 * Available types:
			 *
			 * ```
			 * +------+---------------+---------------------------------------+
			 * | Type | Integer value |                Constant               |
			 * +------+---------------+---------------------------------------+
			 * |  RGB |       1       |                TYPE_RGB               |
			 * +------+---------------+---------------------------------------+
			 * |  HSL |       2       |                TYPE_HSL               |
			 * +------+---------------+---------------------------------------+
			 * ```
			 *
			 * @private
			 * @property {Number}
			 */
			type: 0,

			/**
			 * Hue value. Ranges between 0-360 (inclusive).
			 *
			 * Used in HSL colors.
			 *
			 * @private
			 * @property {Number}
			 */
			hue: 0,

			/**
			 * Saturation value. Ranges between 0-100 (inclusive).
			 *
			 * The value of `0` means that the color will be a shade of gray.
			 *
			 * Used in HSL colors.
			 *
			 * @private
			 * @property {Number}
			 */
			saturation: 0,

			/**
			 * Lightness value. Ranges between 0-100 (inclusive).
			 *
			 * The value of `0` means that the color will be black, while the value of `100`
			 * means that it will be white.
			 *
			 * Used in HSL colors.
			 *
			 * @private
			 * @property {Number}
			 */
			lightness: 0,

			/**
			 * Red channel value. Ranges between 0-255 (inclusive).
			 *
			 * @private
			 * @property {Number}
			 */
			red: 0,

			/**
			 * Green channel value. Ranges between 0-255 (inclusive).
			 *
			 * @private
			 * @property {Number}
			 */
			green: 0,

			/**
			 * Blue channel value. Ranges between 0-255 (inclusive).
			 *
			 * @private
			 * @property {Number}
			 */
			blue: 0,

			/**
			 * Alpha channel value. Ranges between 0-1 (inclusive).
			 *
			 * @private
			 * @property {Number}
			 */
			alpha: 1,

			/**
			 * Blends alpha into RGB color channels. Assumes that background is white.
			 *
			 * @private
			 * @param {Number} red Red channel value.
			 * @param {Number} green Green channel value.
			 * @param {Number} blue Blue channel value.
			 * @param {Number} alpha Alpha channel value.
			 * @returns {Array} Array containing RGB channels with alpha mixed.
			 */
			blendAlphaColor: function( red, green, blue, alpha ) {
				// Based on https://en.wikipedia.org/wiki/Alpha_compositing.
				return CKEDITOR.tools.array.map( [ red, green, blue ], function( color ) {
					return Math.round( CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE - alpha * ( CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE - color ) );
				} );
			},

			/**
			 * Returns color channels formatted as hexadecimal color code preceded by '#'.
			 *
			 * @private
			 * @param {Number} red Red channel value.
			 * @param {Number} green Green channel value.
			 * @param {Number} blue Blue channel value.
			 * @param {Number} [alpha] Optional alpha channel value.
			 * @returns {String} Formatted color value (e.g. `#FF00FF` or with alpha `#FF00FF00`).
			 */
			formatHexString: function( red, green, blue, alpha ) {
				var hexColorCode = '#' + numberToHex( red ) + numberToHex( green ) + numberToHex( blue );

				if ( alpha !== undefined ) {
					hexColorCode += numberToHex( alpha );
				}

				return hexColorCode.toUpperCase();
			},

			/**
			 * Returns color channels formatted as RGB or RGBA color code preceded by given prefix.
			 *
			 * @private
			 * @param {String} rgbPrefix Color code prefix: `rgb` or `rgba`.
			 * @param {Number} red Red channel value.
			 * @param {Number} green Green channel value.
			 * @param {Number} blue Blue channel value.
			 * @param {Number} [alpha] Optional alpha channel value. Should be used with `rgba` prefix only to create valid color value.
			 * @returns {String} Formatted color value (e.g. `rgb(255,255,255)` or with alpha `rgba(255,255,255,1)`).
			 */
			formatRgbString: function( rgbPrefix, red, green, blue, alpha ) {
				var rgba = [ red, green, blue ];

				if ( alpha !== undefined ) {
					rgba.push( alpha );
				}

				return rgbPrefix + '(' + rgba.join( ',' ) + ')';
			},

			/**
			 * Returns color channels formatted as HSL or HSLA color code preceded by given prefix.
			 *
			 * @private
			 * @param {String} hslPrefix Color code prefix: `hsl` or `hsla`.
			 * @param {Number} hue Hue channel value.
			 * @param {Number} saturation Saturation channel value.
			 * @param {Number} lightness Lightness channel value.
			 * @param {Number} [alpha] Optional alpha channel value. Should be used with `hsla` prefix only to create valid color value.
			 * @returns {String} Formatted color value (e.g. `hsl(360,50%,50%)` or `hsla(360,50%,50%,1)`).
			 */
			formatHslString: function( hslPrefix, hue, saturation, lightness, alpha ) {
				var alphaString = alpha !== undefined ? ',' + alpha : '';

				return hslPrefix + '(' +
					hue + ',' +
					saturation + '%,' +
					lightness + '%' +
					alphaString +
					')';
			},

			/**
			 * Parses color code string trying to match it to any supported format and extract RGBA channels.
			 *
			 * @private
			 * @param {String} colorCode Color to parse.
			 */
			parseInput: function( colorCode ) {
				if ( typeof colorCode !== 'string' ) {
					this._.isValidColor = false;
					return;
				}

				colorCode = CKEDITOR.tools.trim( colorCode );

				// Check if named color was passed and get its HEX representation.
				var hexFromNamedColor = this._.matchStringToNamedColor( colorCode );
				if ( hexFromNamedColor ) {
					colorCode = hexFromNamedColor;
				}

				var colorChannelsFromHex = this._.extractColorChannelsFromHex( colorCode ),
					colorChannelsFromRgba = this._.extractColorChannelsFromRgba( colorCode ),
					colorChannelsFromHsla = this._.extractColorChannelsFromHsla( colorCode );

				var colorChannels = colorChannelsFromHex || colorChannelsFromRgba || colorChannelsFromHsla;

				if ( !colorChannels ) {
					this._.isValidColor = false;
					return;
				}

				this._.type = colorChannels.type;
				this._.red = colorChannels.red;
				this._.green = colorChannels.green;
				this._.blue = colorChannels.blue;
				this._.alpha = colorChannels.alpha;

				if ( colorChannels.type === CKEDITOR.tools.color.TYPE_HSL ) {
					this._.hue = colorChannels.hue;
					this._.saturation = colorChannels.saturation;
					this._.lightness = colorChannels.lightness;
				}
			},

			/**
			 * Returns hexadecimal color value from {@link CKEDITOR.tools.color#namedColors} based on provided color name.
			 *
			 * @private
			 * @param {String} colorName color name, e.g. `red`.
			 * @returns {String/null} Hexadecimal color representation or `null` if such named color does not exists.
			 */
			matchStringToNamedColor: function( colorName ) {
				return CKEDITOR.tools.color.namedColors[ colorName.toLowerCase() ] || null;
			},

			/**
			 * Extracts RGBA channels from given HEX string.
			 *
			 * @private
			 * @param {String} colorCode HEX color representation.
			 * @returns {Object/null}
			 */
			extractColorChannelsFromHex: function( colorCode ) {
				// We also like to support hex-like values (so hexes without hash at the beginning).
				if ( colorCode.indexOf( '#' ) === -1 ) {
					colorCode = '#' + colorCode;
				}

				if ( colorCode.match( CKEDITOR.tools.color.hex3CharsRegExp ) ) {
					colorCode = this._.hex3ToHex6( colorCode );
				}

				if ( colorCode.match( CKEDITOR.tools.color.hex4CharsRegExp ) ) {
					colorCode = this._.hex4ToHex8( colorCode );
				}

				if ( !colorCode.match( CKEDITOR.tools.color.hex6CharsRegExp ) && !colorCode.match( CKEDITOR.tools.color.hex8CharsRegExp ) ) {
					return null;
				}

				var parts = colorCode.split( '' ),
					alpha = 1;

				if ( parts[ 7 ] && parts[ 8 ] ) {
					alpha = hexToNumber( parts[ 7 ] + parts[ 8 ] );
					alpha = alpha / CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE;
					alpha = Number( alpha.toFixed( 1 ) );
				}

				return {
					type: CKEDITOR.tools.color.TYPE_RGB,
					red: hexToNumber( parts[ 1 ] + parts[ 2 ] ),
					green: hexToNumber( parts[ 3 ] + parts[ 4 ] ),
					blue: hexToNumber( parts[ 5 ] + parts [ 6 ] ),
					alpha: alpha
				};
			},

			/**
			 * Extracts RGBA channels from given RGB or RGBA string.
			 *
			 * @private
			 * @param {String} colorCode RGB or RGBA color representation.
			 * @returns {Object/null}
			 */
			extractColorChannelsFromRgba: function( colorCode ) {
				var channels =  this._.extractColorChannelsByPattern( colorCode, CKEDITOR.tools.color.rgbRegExp );

				if ( !channels || channels.length < 3 || channels.length > 4 ) {
					return null;
				}

				var isColorDeclaredWithAlpha = channels.length === 4;

				var red = tryToConvertToValidIntegerValue( channels[ 0 ], CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE ),
					green = tryToConvertToValidIntegerValue( channels[ 1 ], CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE ),
					blue = tryToConvertToValidIntegerValue( channels[ 2 ], CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE ),
					alpha = 1;

				if ( isColorDeclaredWithAlpha ) {
					alpha = tryToConvertToValidFloatValue( channels[ 3 ], CKEDITOR.tools.color.MAX_ALPHA_CHANNEL_VALUE );
				}

				var result = {
					type: CKEDITOR.tools.color.TYPE_RGB,
					red: red,
					green: green,
					blue: blue,
					alpha: alpha
				};

				return this._.areColorChannelsValid( red, green, blue, alpha ) ? result : null;
			},

			/**
			 * Extracts RGBA channels from given HSL or HSLA string.
			 *
			 * @private
			 * @param {String} colorCode HSL or HSLA color representation.
			 * @returns {Object/null}
			 */
			extractColorChannelsFromHsla: function( colorCode ) {
				var channels =  this._.extractColorChannelsByPattern( colorCode, CKEDITOR.tools.color.hslRegExp );

				if ( !channels || channels.length < 3 || channels.length > 4 ) {
					return null;
				}

				var isColorDeclaredWithAlpha = channels.length === 4;

				var hue = tryToConvertToValidIntegerValue( channels[ 0 ], CKEDITOR.tools.color.MAX_HUE_CHANNEL_VALUE ),
					saturation = tryToConvertToValidFloatValue( channels[ 1 ], CKEDITOR.tools.color.MAX_SATURATION_LIGHTNESS_CHANNEL_VALUE ),
					lightness = tryToConvertToValidFloatValue( channels[ 2 ], CKEDITOR.tools.color.MAX_SATURATION_LIGHTNESS_CHANNEL_VALUE ),
					alpha = 1;

				var rgba = this._.hslToRgb( hue, saturation, lightness );

				if ( isColorDeclaredWithAlpha ) {
					alpha = tryToConvertToValidFloatValue( channels[ 3 ], CKEDITOR.tools.color.MAX_ALPHA_CHANNEL_VALUE );
				}

				rgba.push( alpha );

				var result = {
					type: CKEDITOR.tools.color.TYPE_HSL,
					red: rgba[ 0 ],
					green: rgba[ 1 ],
					blue: rgba[ 2 ],
					alpha: rgba[ 3 ],
					hue: hue,
					saturation: Math.round( saturation * 100 ),
					lightness: Math.round( lightness * 100 )
				};

				return this._.areColorChannelsValid( rgba[ 0 ], rgba[ 1 ], rgba[ 2 ], rgba[ 3 ] ) ? result : null;
			},

			/**
			 * Converts 3-characters hexadecimal color format to 6-characters one.
			 *
			 * @private
			 * @param {String} hex3ColorCode 3-characters hexadecimal color, e.g. `#F0F`.
			 * @returns {String} 6-characters hexadecimal color e.g. `#FF00FF`.
			 */
			hex3ToHex6: function( hex3ColorCode ) {
				var parts = hex3ColorCode.split( '' );

				return '#' + parts[ 1 ] + parts[ 1 ] + parts[ 2 ] + parts[ 2 ] + parts[ 3 ] + parts[ 3 ];
			},

			/**
			 * Converts 4-characters hexadecimal color format to 8-characters one.
			 *
			 * @private
			 * @param {String} hex4ColorCode 4-characters hexadecimal color, e.g. `#F0F0`.
			 * @returns {String} 8-characters hexadecimal color e.g. `#FF00FF00`.
			 */
			hex4ToHex8: function( hex4ColorCode ) {
				var hex6 = this._.hex3ToHex6( hex4ColorCode.substr( 0, 4 ) );

				return hex6 + CKEDITOR.tools.repeat( hex4ColorCode[ 4 ], 2 );
			},

			/**
			 * Extracts color channels values based on provided regular expression.
			 *
			 * @private
			 * @param {String} value String tested with pattern.
			 * @param {RegExp} regExp Regular expression pattern.
			 * @returns {Array/null} Array with extracted values or null if value doesn't match regular expression.
			 */
			extractColorChannelsByPattern: function( value, regExp ) {
				var match = value.match( regExp );

				if ( !match ) {
					return null;
				}

				var separator = match[ 1 ].indexOf( ',' ) === -1 ? /\s/ : ',',
					values = match[ 1 ].split( separator );

				values = CKEDITOR.tools.array.reduce( values, function( trimmedValues, value ) {
					var trimmedValue = CKEDITOR.tools.trim( value );

					if ( trimmedValue.length === 0 ) {
						return trimmedValues;
					}

					return trimmedValues.concat( [ trimmedValue ] );
				}, [] );

				// There was alpha channel in the RGB no-comma syntax ( / <number>%?) or in HSL value.
				if ( match[ 2 ] ) {
					var alpha = CKEDITOR.tools.trim( match[ 2 ].replace( /[\/,]/, '' ) );

					values.push( alpha );
				}

				return values;
			},

			/**
			 * Validates whether red, green, blue and alpha color channels are within required range.
			 *
			 * For red, green and blue channels range is 0 to {@link CKEDITOR.tools.color#MAX_RGB_CHANNEL_VALUE} inclusive.
			 * For alpha channel range is 0 to {@link CKEDITOR.tools.color#MAX_ALPHA_CHANNEL_VALUE} inclusive.
			 *
			 * @private
			 * @param {Number} red Red channel value.
			 * @param {Number} green Green channel value.
			 * @param {Number} blue Blue channel value.
			 * @param {Number} alpha Alpha channel value.
			 * @returns {Boolean}
			 */
			areColorChannelsValid: function( red, green, blue, alpha ) {
				return isValueWithinRange( red, 0, CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE ) &&
					isValueWithinRange( green, 0, CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE ) &&
					isValueWithinRange( blue, 0, CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE ) &&
					isValueWithinRange( alpha, 0, CKEDITOR.tools.color.MAX_ALPHA_CHANNEL_VALUE );
			},

			/**
			 * Converts HSL color channel values into RGB ones.
			 *
			 * @private
			 * @param {Number} hue
			 * @param {Number} saturation
			 * @param {Number} lightness
			 * @returns {Array} Array of decimal RGB values.
			 */
			hslToRgb: function( hue, saturation, lightness ) {
				// Based on https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB.
				var calculateValueFromConst = function( fixValue ) {
					var k = ( fixValue + ( hue / 30 ) ) % 12,
						a = saturation * Math.min( lightness, 1 - lightness );

					var min = Math.min( k - 3, 9 - k, 1 ),
						max = Math.max( -1, min ),
						normalizedValue = lightness - ( a * max );

					return Math.round( normalizedValue * CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE );
				};

				return [ calculateValueFromConst( 0 ), calculateValueFromConst( 8 ), calculateValueFromConst( 4 ) ];
			},

			/**
			 * Converts RGB color channel values to HSL ones.
			 *
			 * @private
			 * @param {Number} red Number of red channel.
			 * @param {Number} green Number of green channel.
			 * @param {Number} blue Number of blue channel.
			 * @returns {Array} Array of HSL values.
			 */
			rgbToHsl: function( red, green, blue ) {
				// Based on https://en.wikipedia.org/wiki/HSL_and_HSV#General_approach.
				var r = red / CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE,
					g = green / CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE,
					b = blue / CKEDITOR.tools.color.MAX_RGB_CHANNEL_VALUE,
					maxValue = Math.max( r, g, b ),
					minValue = Math.min( r, g, b ),
					chroma = maxValue - minValue;

				var hueFactor = 0;
				switch ( maxValue ) {
					case r:
						hueFactor = ( ( g - b ) / chroma ) % 6;
						break;
					case g:
						hueFactor = ( ( b - r ) / chroma ) + 2;
						break;
					case b:
						hueFactor = ( ( r - g ) / chroma ) + 4;
						break;
				}

				var hue = chroma === 0 ? 0 : 60 * hueFactor,
					lightness = ( maxValue + minValue ) / 2,
					saturation = 0;

				if ( lightness !== 1 && lightness !== 0 ) {
					saturation = chroma / ( 1 - Math.abs( ( 2 * lightness ) - 1 ) );
				}

				hue = Math.round( hue );
				saturation = Math.round( saturation * 100 );
				lightness = Math.round( lightness * 100 );

				return [ hue, saturation, lightness ];
			}
		},

		statics: {
			/**
			 * Indicates that the color is in RGB format.
			 *
			 * @private
			 * @static
			 * @readonly
			 * @property {Number}
			 */
			TYPE_RGB: 1,

			/**
			 * Indicates that the color is in HSL format.
			 *
			 * @private
			 * @static
			 * @readonly
			 * @property {Number}
			 */
			TYPE_HSL: 2,

			/**
			 * The maximum value of RGB channel.
			 *
			 * @private
			 * @static
			 * @readonly
			 * @property {Number}
			 */
			MAX_RGB_CHANNEL_VALUE: 255,

			/**
			 * The maximum value of alpha channel.
			 *
			 * @private
			 * @static
			 * @readonly
			 * @property {Number}
			 */
			MAX_ALPHA_CHANNEL_VALUE: 1,

			/**
			 * The maximum value of hue in HSL color format.
			 *
			 * @private
			 * @static
			 * @readonly
			 * @property {Number}
			 */
			MAX_HUE_CHANNEL_VALUE: 360,

			/**
			 * The maximum value of saturation and lightness in HSL color format.
			 *
			 * @private
			 * @static
			 * @readonly
			 * @property {Number}
			 */
			MAX_SATURATION_LIGHTNESS_CHANNEL_VALUE: 1,

			/**
			 * Regular expression to match hash (`#`) followed by three characters long hexadecimal color value.
			 *
			 * @private
			 * @static
			 * @property {RegExp}
			 */
			hex3CharsRegExp: /#([0-9a-f]{3}$)/gim,

			/**
			 * Regular expression to match hash (`#`) followed by four characters long hexadecimal color value.
			 *
			 * @private
			 * @static
			 * @property {RegExp}
			 */
			hex4CharsRegExp: /#([0-9a-f]{4}$)/gim,

			/**
			 * Regular expression to match hash (`#`) followed by six characters long hexadecimal color value.
			 *
			 * @private
			 * @static
			 * @property {RegExp}
			 */
			hex6CharsRegExp: /#([0-9a-f]{6}$)/gim,

			/**
			 * Regular expression to match hash (`#`) followed by eight characters long hexadecimal color value.
			 *
			 * @private
			 * @static
			 * @property {RegExp}
			 */
			hex8CharsRegExp: /#([0-9a-f]{8}$)/gim,

			/**
			 * Regular expression to extract numbers from RGB and RGBA color value.
			 *
			 * Allowed prefix is `rgb` or `rgba`.
			 * After prefix are values in parentheses. Only dots, coma, digit and percent sign are allowed.
			 *
			 * @private
			 * @static
			 * @property {RegExp}
			 */
			rgbRegExp: /rgba?\(([.,\d\s%]*)(\s*\/\s*[\d.%]+)?\s*\)/i,

			/**
			 * Regular expression to extract numbers from HSL and HSLA color value.
			 *
			 * Allowed prefix is `hsl` or `hsla`.
			 * After prefix are values in parentheses. Only dots, coma, digit and percent sign are allowed.
			 *
			 * @private
			 * @static
			 * @property {RegExp}
			 */
			hslRegExp: /hsla?\((\s*(?:[.\d]+(?:deg)?)(?:\s*,?\s*[.\d]+%){2})((?:(?:\s*\/\s*)|(?:\s*,\s*))[\d.]+%?)?\s*\)/i,
			// Some docs for the above regex:
			// * all values except opacity are in the first capturing group, the opacity is in the second (needed by extraction logic);
			// therefore every subgroup is non-capturing (we need only two capturing groups at the end!)
			// * (?:[.\d]+(?:deg)?) – gets the hue with optional deg unit
			// * (?:\s*,?\s*[.\d]+%){2}) – gets the saturation and lightness as percents and with optional preceding comma
			// * (?:(?:\s*\/\s*)|(?:\s*,\s*)) – gets the opacity separator (comma or slash)

			/**
			 * Color list based on [W3 named colors list](https://www.w3.org/TR/css-color-4/#named-colors).
			 *
			 * @static
			 * @property {Object}
			 */
			namedColors: {
				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',
				windowtext: 'windowtext',
				wheat: '#F5DEB3',
				white: '#FFFFFF',
				whitesmoke: '#F5F5F5',
				yellow: '#FFFF00',
				yellowgreen: '#9ACD32'
			}
		}
	} );

	/**
	 * This list is deprecated, use {@link CKEDITOR.tools.color#namedColors} instead.
	 *
	 * @member CKEDITOR.tools.style.parse
	 * @private
	 * @deprecated 4.16.0
	 */
	CKEDITOR.tools.style.parse._colors = CKEDITOR.tools.color.namedColors;

	// Tries to convert given string into integer value if it is a percentage
	// value (e.g. `99%`) or string containing digits only (e.g. `12345`).
	//
	// If given string represents percentage then it is converted to absolute value based on given `max` value.
	//
	// @param {String} value String to be converted (e.g. `100%` or `20`).
	// @param {Number} max If value is percent, then percentage of max is calculated.
	// @returns {Number/String} Converted value or original value.
	function tryToConvertToValidIntegerValue( value, max ) {
		if ( isPercentValue( value ) ) {
			value = Math.round( max * ( parseFloat( value ) / 100 ) );
		} else if ( isIntegerValue( value ) || isHueValue( value ) ) {
			value = parseInt( value, 10 );
		}

		return value;
	}

	// Tries to convert given string into float value if it is a percentage
	// value (e.g. `99%`) or string containing digits and dot only.
	//
	// If given string represents percentage then it is converted to absolute value based on given `max` value.
	//
	// @param {String} value String to be converted (e.g. `100%`, `0.5` or `.5`).
	// @param {Number} max If value is percent, then percentage of max is calculated.
	// @returns {Number/String} Converted value or original value.
	function tryToConvertToValidFloatValue( value, max ) {
		if ( isPercentValue( value ) ) {
			value = max * ( parseFloat( value ) / 100 );
		} else if ( isFloatValue( value ) ) {
			value = parseFloat( value );
		}

		return value;
	}

	// Validates if given value is a string representing valid float or integer value
	// ending with `%` character (e.g. `1.2%`, `.5%` or `50%`).
	//
	// @param {*} value Any value to be validated.
	// @returns {Boolean}
	function isPercentValue( value ) {
		return typeof value === 'string' && value.match( /^((\d*\.\d+)|(\d+))%{1}$/gm );
	}

	// Validates if given value is a string representing integer value.
	//
	// @param {*} value Any value to be validated.
	// @returns {Boolean}
	function isIntegerValue( value ) {
		return typeof value === 'string' && value.match( /^\d+$/gm );
	}

	// Validates if given value is a string representing hue value (integer or integer + 'deg' keyword).
	//
	// @param {*} value Any value to be validated.
	// @returns {Boolean}
	function isHueValue( value ) {
		return typeof value === 'string' && value.match( /^\d+(?:deg)?$/gm );
	}

	// Validates if given value is a string representing float value.
	//
	// @param {*} value Any value to be validated.
	// @returns {Boolean}
	function isFloatValue( value ) {
		return typeof value === 'string' && value.match( /^\d?\.\d+/gm );
	}

	// Validates if given value is a number (or number-like) within given range (inclusive).
	//
	// @param {*} value Any value to be validated.
	// @param {Number} min The minimum value in the range.
	// @param {Number} max The maximum value in the range
	// @returns {Boolean}
	function isValueWithinRange( value, min, max ) {
		return !isNaN( value ) && value >= min && value <= max;
	}

	// Converts given 10-based value to hexadecimal one.
	//
	// @param {*} value Value to convert.
	// @returns {String} Hexadecimal value.
	function numberToHex( value ) {
		var hex = value.toString( 16 );

		return hex.length == 1 ? '0' + hex : hex;
	}

	// Convert hexadecimal value to 10-based one.
	//
	// @param {String} hexValue Value to convert.
	// @returns {Number}
	function hexToNumber( hexValue ) {
		return parseInt( hexValue, 16 );
	}

} )();
