<?php

namespace Mpdf\Color;

use Mpdf\Mpdf;

class ColorConverter
{

	const MODE_GRAYSCALE = 1;

	const MODE_SPOT = 2;

	const MODE_RGB = 3;

	const MODE_CMYK = 4;

	const MODE_RGBA = 5;

	const MODE_CMYKA = 6;

	private $mpdf;

	private $colorModeConverter;

	private $colorSpaceRestrictor;

	private $cache;

	public function __construct(Mpdf $mpdf, ColorModeConverter $colorModeConverter, ColorSpaceRestrictor $colorSpaceRestrictor)
	{
		$this->mpdf = $mpdf;
		$this->colorModeConverter = $colorModeConverter;
		$this->colorSpaceRestrictor = $colorSpaceRestrictor;

		$this->cache = [];
	}

	public function convert($color, array &$PDFAXwarnings = [])
	{
		$color = strtolower(trim($color));

		if ($color === 'transparent' || $color === 'inherit') {
			return false;
		}

		if (isset(NamedColors::$colors[$color])) {
			$color = NamedColors::$colors[$color];
		}

		if (!isset($this->cache[$color])) {
			$c = $this->convertPlain($color, $PDFAXwarnings);
			$cstr = '';
			if (is_array($c)) {
				$c = array_pad($c, 6, 0);
				$cstr = pack(
					'a1ccccc',
					$c[0],
					round($c[1]) & 0xFF,
					round($c[2]) & 0xFF,
					round($c[3]) & 0xFF,
					round($c[4]) & 0xFF,
					round($c[5]) & 0xFF
				);
			}

			$this->cache[$color] = $cstr;
		}

		return $this->cache[$color];
	}

	public function lighten($c)
	{
		$this->ensureBinaryColorFormat($c);

		if ($c[0] == static::MODE_RGB || $c[0] == static::MODE_RGBA) {
			list($h, $s, $l) = $this->colorModeConverter->rgb2hsl(ord($c[1]) / 255, ord($c[2]) / 255, ord($c[3]) / 255);
			$l += ((1 - $l) * 0.8);
			list($r, $g, $b) = $this->colorModeConverter->hsl2rgb($h, $s, $l);
			$ret = [3, $r, $g, $b];
		} elseif ($c[0] == static::MODE_CMYK || $c[0] == static::MODE_CMYKA) {
			$ret = [4, max(0, ord($c[1]) - 20), max(0, ord($c[2]) - 20), max(0, ord($c[3]) - 20), max(0, ord($c[4]) - 20)];
		} elseif ($c[0] == static::MODE_GRAYSCALE) {
			$ret = [1, min(255, ord($c[1]) + 32)];
		}

		$c = array_pad($ret, 6, 0);
		$cstr = pack(
			'a1ccccc',
			$c[0],
			round($c[1]) & 0xFF,
			round($c[2]) & 0xFF,
			round($c[3]) & 0xFF,
			round($c[4]) & 0xFF,
			round($c[5]) & 0xFF
		);

		return $cstr;
	}

	public function darken($c)
	{
		$this->ensureBinaryColorFormat($c);

		if ($c[0] == static::MODE_RGB || $c[0] == static::MODE_RGBA) {
			list($h, $s, $l) = $this->colorModeConverter->rgb2hsl(ord($c[1]) / 255, ord($c[2]) / 255, ord($c[3]) / 255);
			$s *= 0.25;
			$l *= 0.75;
			list($r, $g, $b) = $this->colorModeConverter->hsl2rgb($h, $s, $l);
			$ret = [3, $r, $g, $b];
		} elseif ($c[0] == static::MODE_CMYK || $c[0] == static::MODE_CMYKA) {
			$ret = [4, min(100, ord($c[1]) + 20), min(100, ord($c[2]) + 20), min(100, ord($c[3]) + 20), min(100, ord($c[4]) + 20)];
		} elseif ($c[0] == static::MODE_GRAYSCALE) {
			$ret = [1, max(0, ord($c[1]) - 32)];
		}
		$c = array_pad($ret, 6, 0);
		$cstr = pack('a1ccccc', $c[0], $c[1] & 0xFF, $c[2] & 0xFF, $c[3] & 0xFF, $c[4] & 0xFF, $c[5] & 0xFF);

		return $cstr;
	}

	/**
	 * @param string $c
	 * @return float[]
	 */
	public function invert($c)
	{
		$this->ensureBinaryColorFormat($c);

		if ($c[0] == static::MODE_RGB || $c[0] == static::MODE_RGBA) {
			return [3, 255 - ord($c[1]), 255 - ord($c[2]), 255 - ord($c[3])];
		}

		if ($c[0] == static::MODE_CMYK || $c[0] == static::MODE_CMYKA) {
			return [4, 100 - ord($c[1]), 100 - ord($c[2]), 100 - ord($c[3]), 100 - ord($c[4])];
		}

		if ($c[0] == static::MODE_GRAYSCALE) {
			return [1, 255 - ord($c[1])];
		}

		// Cannot cope with non-RGB colors at present
		throw new \Mpdf\MpdfException('Trying to invert non-RGB color');
	}

	/**
	 * @param string $c Binary color string
	 *
	 * @return string
	 */
	public function colAtoString($c)
	{
		if ($c[0] == static::MODE_GRAYSCALE) {
			return 'rgb(' . ord($c[1]) . ', ' . ord($c[1]) . ', ' . ord($c[1]) . ')';
		}

		if ($c[0] == static::MODE_SPOT) {
			return 'spot(' . ord($c[1]) . ', ' . ord($c[2]) . ')';
		}

		if ($c[0] == static::MODE_RGB) {
			return 'rgb(' . ord($c[1]) . ', ' . ord($c[2]) . ', ' . ord($c[3]) . ')';
		}

		if ($c[0] == static::MODE_CMYK) {
			return 'cmyk(' . ord($c[1]) . ', ' . ord($c[2]) . ', ' . ord($c[3]) . ', ' . ord($c[4]) . ')';
		}

		if ($c[0] == static::MODE_RGBA) {
			return 'rgba(' . ord($c[1]) . ', ' . ord($c[2]) . ', ' . ord($c[3]) . ', ' . sprintf('%0.2F', ord($c[4]) / 100) . ')';
		}

		if ($c[0] == static::MODE_CMYKA) {
			return 'cmyka(' . ord($c[1]) . ', ' . ord($c[2]) . ', ' . ord($c[3]) . ', ' . ord($c[4]) . ', ' . sprintf('%0.2F', ord($c[5]) / 100) . ')';
		}

		return '';
	}

	/**
	 * @param string $color
	 * @param string[] $PDFAXwarnings
	 *
	 * @return bool|float[]
	 */
	private function convertPlain($color, array &$PDFAXwarnings = [])
	{
		$c = false;

		if (preg_match('/^[\d]+$/', $color)) {
			$c = [static::MODE_GRAYSCALE, $color]; // i.e. integer only
		} elseif (strpos($color, '#') === 0) { // case of #nnnnnn or #nnn
			$c = $this->processHashColor($color);
		} elseif (preg_match('/(rgba|rgb|device-cmyka|cmyka|device-cmyk|cmyk|hsla|hsl|spot)\((.*?)\)/', $color, $m)) {
			// ignore colors containing CSS variables
			if (str_starts_with(mb_strtolower($m[2]), 'var(--')) {
				$m[2] = '0, 0, 0, 100';
			}
			$c = $this->processModeColor($m[1], explode(',', $m[2]));
		}

		if ($this->mpdf->PDFA || $this->mpdf->PDFX || $this->mpdf->restrictColorSpace) {
			$c = $this->restrictColorSpace($c, $color, $PDFAXwarnings);
		}

		return $c;
	}

	/**
	 * @param string $color
	 *
	 * @return float[]
	 */
	private function processHashColor($color)
	{
		// in case of Background: #CCC url() x-repeat etc.
		$cor = preg_replace('/\s+.*/', '', $color);

		// Turn #RGB into #RRGGBB
		if (strlen($cor) === 4) {
			$cor = '#' . $cor[1] . $cor[1] . $cor[2] . $cor[2] . $cor[3] . $cor[3];
		}

		$r = self::safeHexDec(substr($cor, 1, 2));
		$g = self::safeHexDec(substr($cor, 3, 2));
		$b = self::safeHexDec(substr($cor, 5, 2));

		return [3, $r, $g, $b];
	}

	/**
	 * @param $mode
	 * @param mixed[] $cores
	 * @return bool|float[]
	 */
	private function processModeColor($mode, array $cores)
	{
		$c = false;

		$cores = $this->convertPercentCoreValues($mode, $cores);

		switch ($mode) {
			case 'rgb':
				return [static::MODE_RGB, $cores[0], $cores[1], $cores[2]];

			case 'rgba':
				return [static::MODE_RGBA, $cores[0], $cores[1], $cores[2], $cores[3] * 100];

			case 'cmyk':
			case 'device-cmyk':
				return [static::MODE_CMYK, $cores[0], $cores[1], $cores[2], $cores[3]];

			case 'cmyka':
			case 'device-cmyka':
				return [static::MODE_CMYKA, $cores[0], $cores[1], $cores[2], $cores[3], $cores[4] * 100];

			case 'hsl':
				$conv = $this->colorModeConverter->hsl2rgb($cores[0] / 360, $cores[1], $cores[2]);
				return [static::MODE_RGB, $conv[0], $conv[1], $conv[2]];

			case 'hsla':
				$conv = $this->colorModeConverter->hsl2rgb($cores[0] / 360, $cores[1], $cores[2]);
				return [static::MODE_RGBA, $conv[0], $conv[1], $conv[2], $cores[3] * 100];

			case 'spot':
				$name = strtoupper(trim($cores[0]));

				if (!isset($this->mpdf->spotColors[$name])) {
					if (isset($cores[5])) {
						$this->mpdf->AddSpotColor($cores[0], $cores[2], $cores[3], $cores[4], $cores[5]);
					} else {
						throw new \Mpdf\MpdfException(sprintf('Undefined spot color "%s"', $name));
					}
				}

				return [static::MODE_SPOT, $this->mpdf->spotColors[$name]['i'], $cores[1]];
		}

		return $c;
	}

	/**
	 * @param string $mode
	 * @param mixed[] $cores
	 *
	 * @return float[]
	 */
	private function convertPercentCoreValues($mode, array $cores)
	{
		$ncores = count($cores);

		if (strpos($cores[0], '%') !== false) {
			$cores[0] = (float) $cores[0];
			if ($mode === 'rgb' || $mode === 'rgba') {
				$cores[0] = (int) ($cores[0] * 255 / 100);
			}
		}

		if ($ncores > 1 && strpos($cores[1], '%') !== false) {
			$cores[1] = (float) $cores[1];
			if ($mode === 'rgb' || $mode === 'rgba') {
				$cores[1] = (int) ($cores[1] * 255 / 100);
			}
			if ($mode === 'hsl' || $mode === 'hsla') {
				$cores[1] /= 100;
			}
		}

		if ($ncores > 2 && strpos($cores[2], '%') !== false) {
			$cores[2] = (float) $cores[2];
			if ($mode === 'rgb' || $mode === 'rgba') {
				$cores[2] = (int) ($cores[2] * 255 / 100);
			}
			if ($mode === 'hsl' || $mode === 'hsla') {
				$cores[2] /= 100;
			}
		}

		if ($ncores > 3 && strpos($cores[3], '%') !== false) {
			$cores[3] = (float) $cores[3];
		}

		return $cores;
	}

	/**
	 * @param mixed $c
	 * @param string $color
	 * @param string[] $PDFAXwarnings
	 *
	 * @return float[]
	 */
	private function restrictColorSpace($c, $color, &$PDFAXwarnings = [])
	{
		return $this->colorSpaceRestrictor->restrictColorSpace($c, $color, $PDFAXwarnings);
	}

	/**
	 * @param string $color Binary color string
	 */
	private function ensureBinaryColorFormat($color)
	{
		if (!is_string($color)) {
			throw new \Mpdf\MpdfException('Invalid color input, binary color string expected');
		}

		if (strlen($color) !== 6) {
			throw new \Mpdf\MpdfException('Invalid color input, binary color string expected');
		}

		if (!in_array($color[0], [static::MODE_GRAYSCALE, static::MODE_SPOT, static::MODE_RGB, static::MODE_CMYK, static::MODE_RGBA, static::MODE_CMYKA])) {
			throw new \Mpdf\MpdfException('Invalid color input, invalid color mode in binary color string');
		}
	}

	/**
	 * Converts the given hexString to its decimal representation when all digits are hexadecimal
	 *
	 * @param string $hexString The hexadecimal string to convert
	 * @return float|int The decimal representation of hexString or 0 if not all digits of hexString are hexadecimal
	 */
	private function safeHexDec($hexString)
	{
		return ctype_xdigit($hexString) ? hexdec($hexString) : 0;
	}
}
