<?php

/**
 * Author: Arlo Carreon <http://arlocarreon.com>
 * Info: http://mexitek.github.io/phpColors/
 * License: http://arlo.mit-license.org/
 */

/**
 * A color utility that helps manipulate HEX colors
 */
class VamtamColor {

	private $_hex;
	private $_hsl;
	private $_rgb;
	private $_rgba;
	private $_alpha;

	private $constants = [
		'white' => '#fff',
		'black' => '#000',
	];

	/**
	 * Auto darkens/lightens by 10% for sexily-subtle gradients.
	 * Set this to FALSE to adjust automatic shade to be between given color
	 * and black (for darken) or white (for lighten)
	 */
	const DEFAULT_ADJUST = 10;

	/**
	 * Instantiates the class with a HEX value
	 * @param string $hex
	 * @throws Exception "Bad color format"
	 */
	function __construct( $hex ) {

		if ( isset( $this->constants[ $hex ] ) ) {
			$hex = $this->constants[ $hex ];
		}

		// Strip # sign is present
		$color = str_replace( '#', '', $hex );
		$alpha = null;

		// Make sure it's 6 digits
		if ( strlen( $color ) === 3 ) {
			$color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2];
		} elseif ( strlen( $color ) === 8 ) {
			$alpha =  $color[6] .  $color[7];
			$color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2];
		} elseif ( strlen( $color ) != 6 ) {
			trigger_error( "HEX color needs to be 8, 6 or 3 digits long, got '$color'", E_USER_WARNING );
		}

		$this->_hsl = self::hexToHsl( $color );
		$this->_hex = $color;
		$this->_rgb = self::hexToRgb( $color );

		if ( isset( $alpha ) ) {
			$this->_alpha = hexdec( $alpha );
			$this->_rgba  = self::hexToRgba( $color, $this->_alpha );
		}
	}

	// ====================
	// = Public Interface =
	// ====================

	/**
	 * Given a HEX string returns a HSL array equivalent.
	 * @param string $color
	 * @return array HSL associative array
	 */
	public static function hexToHsl( $color ) {

		// Sanity check
		$color = self::_checkHex( $color );

		// Convert HEX to DEC
		$R = hexdec( $color[0] . $color[1] );
		$G = hexdec( $color[2] . $color[3] );
		$B = hexdec( $color[4] . $color[5] );

		$HSL = array();

		$var_R = ($R / 255);
		$var_G = ($G / 255);
		$var_B = ($B / 255);

		$var_Min = min( $var_R, $var_G, $var_B );
		$var_Max = max( $var_R, $var_G, $var_B );
		$del_Max = $var_Max - $var_Min;

		$L = ($var_Max + $var_Min) / 2;

		if ( $del_Max == 0 ) {
			$H = 0;
			$S = 0;
		} else {
			if ( $L < 0.5 ) $S = $del_Max / ( $var_Max + $var_Min );
			else $S            = $del_Max / ( 2 - $var_Max - $var_Min );

			$del_R = ( ( ( $var_Max - $var_R ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
			$del_G = ( ( ( $var_Max - $var_G ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
			$del_B = ( ( ( $var_Max - $var_B ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;

			if ($var_R == $var_Max) $H     = $del_B - $del_G;
			elseif ($var_G == $var_Max) $H = ( 1 / 3 ) + $del_R - $del_B;
			elseif ($var_B == $var_Max) $H = ( 2 / 3 ) + $del_G - $del_R;

			if ($H < 0) $H++;
			if ($H > 1) $H--;
		}

		$HSL['H'] = ($H * 360);
		$HSL['S'] = $S;
		$HSL['L'] = $L;

		return $HSL;
	}

	/**
	 *  Given a HSL associative array returns the equivalent HEX string
	 * @param array $hsl
	 * @return string HEX string
	 * @throws Exception "Bad HSL Array"
	 */
	public static function hslToHex( $hsl = array() ) {
		 // Make sure it's HSL
		if ( empty( $hsl ) || ! isset( $hsl['H'] ) || ! isset( $hsl['S'] ) || ! isset( $hsl['L'] ) ) {
			throw new Exception( 'Param was not an HSL array' );
		}

		list($H,$S,$L) = array( $hsl['H'] / 360, $hsl['S'], $hsl['L'] );

		if ( $S == 0 ) {
			$r = $L * 255;
			$g = $L * 255;
			$b = $L * 255;
		} else {

			if ( $L < 0.5 ) {
				$var_2 = $L * (1 + $S);
			} else {
				$var_2 = ($L + $S) - ($S * $L);
			}

			$var_1 = 2 * $L - $var_2;

			$r = round( 255 * self::_huetorgb( $var_1, $var_2, $H + (1 / 3) ) );
			$g = round( 255 * self::_huetorgb( $var_1, $var_2, $H ) );
			$b = round( 255 * self::_huetorgb( $var_1, $var_2, $H - (1 / 3) ) );

		}

		// Convert to hex
		$r = dechex( $r );
		$g = dechex( $g );
		$b = dechex( $b );

		// Make sure we get 2 digits for decimals
		$r = (strlen( '' . $r ) === 1) ? '0' . $r:$r;
		$g = (strlen( '' . $g ) === 1) ? '0' . $g:$g;
		$b = (strlen( '' . $b ) === 1) ? '0' . $b:$b;

		return $r . $g . $b;
	}


	/**
	 * Given a HEX string returns a RGB array equivalent.
	 * @param string $color
	 * @return array RGB associative array
	 */
	public static function hexToRgb( $color ) {

		// Sanity check
		$color = self::_checkHex( $color );

		// Convert HEX to DEC
		$R = hexdec( $color[0] . $color[1] );
		$G = hexdec( $color[2] . $color[3] );
		$B = hexdec( $color[4] . $color[5] );

		$RGB['R'] = $R;
		$RGB['G'] = $G;
		$RGB['B'] = $B;

		return $RGB;
	}

	/**
	 * Given a HEX string and an alpha channel, returns a RGBA array equivalent.
	 * @param string $color, $alpha
	 * @return array RGBA associative array
	 */
	public static function hexToRgba( $color, $alpha = 255 ) {
		
		// Sanity check
		$color = self::_checkHex( $color );

		// Convert HEX to DEC
		$R = hexdec( $color[0] . $color[1] );
		$G = hexdec( $color[2] . $color[3] );
		$B = hexdec( $color[4] . $color[5] );
		$A = round( $alpha / 255, 2 );

		$RGBA['R'] = $R;
		$RGBA['G'] = $G;
		$RGBA['B'] = $B;
		$RGBA['A'] = $A;

		return $RGBA;
	}


	/**
	 *  Given an RGB associative array returns the equivalent HEX string
	 * @param array $rgb
	 * @return string RGB string
	 * @throws Exception "Bad RGB Array"
	 */
	public static function rgbToHex( $rgb = array() ) {
		 // Make sure it's RGB
		if ( empty( $rgb ) || ! isset( $rgb['R'] ) || ! isset( $rgb['G'] ) || ! isset( $rgb['B'] ) ) {
			throw new Exception( 'Param was not an RGB array' );
		}

		// https://github.com/mexitek/phpColors/issues/25#issuecomment-88354815
		// Convert RGB to HEX
		$hex[0] = str_pad( dechex( $rgb['R'] ), 2, '0', STR_PAD_LEFT );
		$hex[1] = str_pad( dechex( $rgb['G'] ), 2, '0', STR_PAD_LEFT );
		$hex[2] = str_pad( dechex( $rgb['B'] ), 2, '0', STR_PAD_LEFT );

		return implode( '', $hex );

  	}

	// For a given color, return it's contrast one.
	public static function get_contrast_color( $hex ) {
		$color    = new self( $hex );
		$hc_color = '';
		if ( $color->luminance > 0.4 ) {
			$hc_color = '#000000';
		} else {
			$hc_color = '#FFFFFF';
		}
		return $hc_color;
	}


	/**
	 * Given a HEX value, returns a darker color. If no desired amount provided, then the color halfway between
	 * given HEX and black will be returned.
	 * @param int $amount
	 * @return string Darker HEX value
	 */
	public function darken( $amount = self::DEFAULT_ADJUST ) {
		// Darken
		$darkerHSL = $this->_darken( $this->_hsl, $amount );
		// Return as HEX
		return self::hslToHex( $darkerHSL );
	}

	/**
	 * Given a HEX value, returns a lighter color. If no desired amount provided, then the color halfway between
	 * given HEX and white will be returned.
	 * @param int $amount
	 * @return string Lighter HEX value
	 */
	public function lighten( $amount = self::DEFAULT_ADJUST ) {
		// Lighten
		$lighterHSL = $this->_lighten( $this->_hsl, $amount );
		// Return as HEX
		return self::hslToHex( $lighterHSL );
	}

	/**
	 * Given a HEX value, returns a mixed color. If no desired amount provided, then the color mixed by this ratio
	 * @param string $hex2 Secondary HEX value to mix with
	 * @param int $amount = -100..0..+100
	 * @return string mixed HEX value
	 */
	public function mix( $hex2, $amount = 0 ) {
		$rgb2  = self::hexToRgb( $hex2 );
		$mixed = $this->_mix( $this->_rgb, $rgb2, $amount );
		// Return as HEX
		return self::rgbToHex( $mixed );
	}

	/**
	 * Creates an array with two shades that can be used to make a gradient
	 * @param int $amount Optional percentage amount you want your contrast color
	 * @return array An array with a 'light' and 'dark' index
	 */
	public function makeGradient( $amount = self::DEFAULT_ADJUST ) {
		// Decide which color needs to be made
		if ( $this->isLight() ) {
			$lightColor = $this->_hex;
			$darkColor  = $this->darken( $amount );
		} else {
			$lightColor = $this->lighten( $amount );
			$darkColor  = $this->_hex;
		}

		// Return our gradient array
		return array(
			'light' => $lightColor,
			'dark' => $darkColor,
		);
	}


	/**
	 * Returns whether or not given color is considered "light"
	 * @param string|Boolean $color
	 * @param int $lighterThan
	 * @return boolean
	 */
	public function isLight( $color = false, $lighterThan = 130 ) {
		// Get our color
		$color = ($color) ? $color : $this->_hex;

		// Calculate straight from rbg
		$r = hexdec( $color[0] . $color[1] );
		$g = hexdec( $color[2] . $color[3] );
		$b = hexdec( $color[4] . $color[5] );

		return (( $r * 299 + $g * 587 + $b * 114 ) / 1000 > $lighterThan);
	}

	/**
	 * Returns whether or not a given color is considered "dark"
	 * @param string|Boolean $color
	 * @param int $darkerThan
	 * @return boolean
	 */
	public function isDark( $color = false, $darkerThan = 130 ) {
		// Get our color
		$color = ($color) ? $color:$this->_hex;

		// Calculate straight from rbg
		$r = hexdec( $color[0] . $color[1] );
		$g = hexdec( $color[2] . $color[3] );
		$b = hexdec( $color[4] . $color[5] );

		return (( $r * 299 + $g * 587 + $b * 114 ) / 1000 <= $darkerThan);
	}

	/**
	 * Returns the complimentary color
	 * @return string Complementary hex color
	 *
	 */
	public function complementary() {
		// Get our HSL
		$hsl = $this->_hsl;

		// Adjust Hue 180 degrees
		$hsl['H'] += ($hsl['H'] > 180) ? -180:180;

		// Return the new value in HEX
		return self::hslToHex( $hsl );
	}

	/**
	 * Returns your color's HSL array
	 */
	public function getHsl() {
		return $this->_hsl;
	}
	/**
	 * Returns your original color
	 */
	public function getHex() {
		return $this->_hex;
	}
	/**
	 * Returns your color's RGB array
	 */
	public function getRgb() {
		return $this->_rgb;
	}

	/**
	 * Returns the cross browser CSS3 gradient
	 * @param int $amount Optional: percentage amount to light/darken the gradient
	 * @param boolean $vintageBrowsers Optional: include vendor prefixes for browsers that almost died out already
	 * @param string $prefix Optional: prefix for every lines
	 * @param string $suffix Optional: suffix for every lines
	 * @link  http://caniuse.com/css-gradients Resource for the browser support
	 * @return string CSS3 gradient for chrome, safari, firefox, opera and IE10
	 */
	public function getCssGradient( $amount = self::DEFAULT_ADJUST, $vintageBrowsers = false, $suffix = '', $prefix = '' ) {

		// Get the recommended gradient
		$g = $this->makeGradient( $amount );

		$css = '';
		/* fallback/image non-cover color */
		$css .= "{$prefix}background-color: #" . $this->_hex . ";{$suffix}";

		/* IE Browsers */
		$css .= "{$prefix}filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#" . $g['light'] . "', endColorstr='#" . $g['dark'] . "');{$suffix}";

		/* Safari 4+, Chrome 1-9 */
		if ( $vintageBrowsers ) {
			$css .= "{$prefix}background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#" . $g['light'] . '), to(#' . $g['dark'] . "));{$suffix}";
		}

		/* Safari 5.1+, Mobile Safari, Chrome 10+ */
		$css .= "{$prefix}background-image: -webkit-linear-gradient(top, #" . $g['light'] . ', #' . $g['dark'] . ");{$suffix}";

		/* Firefox 3.6+ */
		if ( $vintageBrowsers ) {
			$css .= "{$prefix}background-image: -moz-linear-gradient(top, #" . $g['light'] . ', #' . $g['dark'] . ");{$suffix}";
		}

		/* Opera 11.10+ */
		if ( $vintageBrowsers ) {
			$css .= "{$prefix}background-image: -o-linear-gradient(top, #" . $g['light'] . ', #' . $g['dark'] . ");{$suffix}";
		}

		/* Unprefixed version (standards): FF 16+, IE10+, Chrome 26+, Safari 7+, Opera 12.1+ */
		$css .= "{$prefix}background-image: linear-gradient(to bottom, #" . $g['light'] . ', #' . $g['dark'] . ");{$suffix}";

		// Return our CSS
		return $css;
	}

	// ===========================
	// = Private Functions Below =
	// ===========================


	/**
	 * Darkens a given HSL array
	 * @param array $hsl
	 * @param int $amount
	 * @return array $hsl
	 */
	private function _darken( $hsl, $amount = self::DEFAULT_ADJUST ) {
		// Check if we were provided a number
		if ( $amount ) {
			$hsl['L'] = ($hsl['L'] * 100) - $amount;
			$hsl['L'] = ($hsl['L'] < 0) ? 0:$hsl['L'] / 100;
		} else {
			// We need to find out how much to darken
			$hsl['L'] = $hsl['L'] / 2 ;
		}

		return $hsl;
	}

	/**
	 * Lightens a given HSL array
	 * @param array $hsl
	 * @param int $amount
	 * @return array $hsl
	 */
	private function _lighten( $hsl, $amount = self::DEFAULT_ADJUST ) {
		// Check if we were provided a number
		if ( $amount ) {
			$hsl['L'] = ($hsl['L'] * 100) + $amount;
			$hsl['L'] = ($hsl['L'] > 100) ? 1:$hsl['L'] / 100;
		} else {
			// We need to find out how much to lighten
			$hsl['L'] += (1 -$hsl['L']) / 2;
		}

		return $hsl;
	}

	/**
	 * Mix 2 rgb colors and return an rgb color
	 * @param array $rgb1
	 * @param array $rgb2
	 * @param int $amount ranged -100..0..+100
	 * @return array $rgb
	 *
	 * 	ported from http://phpxref.pagelines.com/nav.html?includes/class.colors.php.source.html
	 */
	private function _mix( $rgb1, $rgb2, $amount = 0 ) {

		 $r1 = ($amount + 100) / 100;
		 $r2 = 2 - $r1;

		 $rmix = (($rgb1['R'] * $r1) + ($rgb2['R'] * $r2)) / 2;
		 $gmix = (($rgb1['G'] * $r1) + ($rgb2['G'] * $r2)) / 2;
		 $bmix = (($rgb1['B'] * $r1) + ($rgb2['B'] * $r2)) / 2;

		 return array(
			 'R' => $rmix,
			 'G' => $gmix,
			 'B' => $bmix,
		 );
	}

	/**
	 * Returns the luminance as defined by WCAG 2.0
	 * @return float
	 */
	private function getLuminance() {
		// Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
		$map = array( 'R', 'G', 'B' );
		$intermediate = array();

		foreach ( $map as $comp ) {
			$value = $this->_rgb[ $comp ];

			$value /= 255;

			$intermediate[ $comp ] =  $value < .03928 ? $value / 12.92 : pow( ( $value + .055 ) / 1.055, 2.4 );
		}

		return .2126 * $intermediate['R'] + .7152 * $intermediate['G'] + 0.0722 * $intermediate['B'];
	}

	/**
	 * Returns the contrast ratio between the current color and a given relative luminance
	 * @param  float $L2 relative luminance
	 * @return float
	 */
	public function getContrastRatio( $L2 ) {
		// Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
		$L1 = $this->getLuminance();

		return ( max( $L1, $L2 ) + 0.05 ) / ( min( $L1, $L2 ) + 0.05 );
	}

	/**
	 * Given a Hue, returns corresponding RGB value
	 * @param int $v1
	 * @param int $v2
	 * @param int $vH
	 * @return int
	 */
	private static function _huetorgb( $v1, $v2, $vH ) {
		if ( $vH < 0 ) {
			$vH += 1;
		}

		if ( $vH > 1 ) {
			$vH -= 1;
		}

		if ( (6 * $vH) < 1 ) {
			   return ($v1 + ($v2 - $v1) * 6 * $vH);
		}

		if ( (2 * $vH) < 1 ) {
			return $v2;
		}

		if ( (3 * $vH) < 2 ) {
			return ($v1 + ($v2 -$v1) * ( (2 / 3) -$vH ) * 6);
		}

		return $v1;

	}

	/**
	 * You need to check if you were given a good hex string
	 * @param string $hex
	 * @return string Color
	 * @throws Exception "Bad color format"
	 */
	private static function _checkHex( $hex ) {
		// Strip # sign is present
		$color = str_replace( '#', '', $hex );

		// Make sure it's 6 digits
		if ( strlen( $color ) == 3 ) {
			$color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2];
		} elseif ( strlen( $color ) != 6 ) {
			throw new Exception( 'HEX color needs to be 6 or 3 digits long' );
		}

		return $color;
	}

	/**
	 * Converts object into its string representation
	 * @return string Color
	 */
	public function __toString() {
		return '#' . $this->getHex();
	}

	public function __get( $name ) {
		switch ( strtolower( $name ) ) {
			case 'red':
			case 'r':
				return $this->_rgb['R'];
			case 'green':
			case 'g':
				return $this->_rgb['G'];
			case 'blue':
			case 'b':
				return $this->_rgb['B'];
			case 'hue':
			case 'h':
				return $this->_hsl['H'];
			case 'saturation':
			case 's':
				return $this->_hsl['S'];
			case 'lightness':
			case 'l':
				return $this->_hsl['L'];
			case 'luminance':
				return $this->getLuminance();
		}
	}

	public function __set( $name, $value ) {
		switch ( strtolower( $name ) ) {
			case 'red':
			case 'r':
				$this->_rgb['R'] = $value;
				$this->_hex      = $this->rgbToHex( $this->_rgb );
				$this->_hsl      = $this->hexToHsl( $this->_hex );
				break;
			case 'green':
			case 'g':
				$this->_rgb['G'] = $value;
				$this->_hex      = $this->rgbToHex( $this->_rgb );
				$this->_hsl      = $this->hexToHsl( $this->_hex );
				break;
			case 'blue':
			case 'b':
				$this->_rgb['B'] = $value;
				$this->_hex      = $this->rgbToHex( $this->_rgb );
				$this->_hsl      = $this->hexToHsl( $this->_hex );
				break;
			case 'hue':
			case 'h':
				$this->_hsl['H'] = $value;
				$this->_hex      = $this->hslToHex( $this->_hsl );
				$this->_rgb      = $this->hexToRgb( $this->_hex );
				break;
			case 'saturation':
			case 's':
				$this->_hsl['S'] = $value;
				$this->_hex      = $this->hslToHex( $this->_hsl );
				$this->_rgb      = $this->hexToRgb( $this->_hex );
				break;
			case 'lightness':
			case 'light':
			case 'l':
				$this->_hsl['L'] = $value;
				$this->_hex      = $this->hslToHex( $this->_hsl );
				$this->_rgb      = $this->hexToRgb( $this->_hex );
				break;
		}
	}
}

