<?php
/**
 * Class QREps
 *
 * @created      09.05.2022
 * @author       smiley <smiley@chillerlan.net>
 * @copyright    2022 smiley
 * @license      MIT
 */
declare(strict_types=1);

namespace chillerlan\QRCode\Output;

use function array_values, count, date, implode, is_array, is_numeric, max, min, round, sprintf;

/**
 * Encapsulated Postscript (EPS) output
 *
 * @see https://github.com/t0k4rt/phpqrcode/blob/bb29e6eb77e0a2a85bb0eb62725e0adc11ff5a90/qrvect.php#L52-L137
 * @see https://web.archive.org/web/20170818010030/http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/postscript/pdfs/5002.EPSF_Spec.pdf
 * @see https://web.archive.org/web/20210419003859/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/PLRM.pdf
 * @see https://github.com/chillerlan/php-qrcode/discussions/148
 */
class QREps extends QROutputAbstract{

	final public const MIME_TYPE = 'application/postscript';

	public static function moduleValueIsValid(mixed $value):bool{

		if(!is_array($value) || count($value) < 3){
			return false;
		}

		// check the first values of the array
		foreach(array_values($value) as $i => $val){

			if($i > 3){
				break;
			}

			if(!is_numeric($val)){
				return false;
			}

		}

		return true;
	}

	protected function prepareModuleValue(mixed $value):string{
		$values = [];

		foreach(array_values($value) as $i => $val){

			if($i > 3){
				break;
			}

			// clamp value and convert from int 0-255 to float 0-1 RGB/CMYK range
			$values[] = round((max(0, min(255, intval($val))) / 255), 6);
		}

		return $this->formatColor($values);
	}

	protected function getDefaultModuleValue(bool $isDark):string{
		return $this->formatColor(($isDark) ? [0.0, 0.0, 0.0] : [1.0, 1.0, 1.0]);
	}

	/**
	 * Set the color format string
	 *
	 * 4 values in the color array will be interpreted as CMYK, 3 as RGB
	 *
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
	 *
	 * @param float[] $values
	 */
	protected function formatColor(array $values):string{
		$count = count($values);

		if($count < 3){
			throw new QRCodeOutputException('invalid color value');
		}

		// the EPS functions "C" and "R" are defined in the header below
		$format = ($count === 4)
			? '%f %f %f %f C' // CMYK
			: '%f %f %f R'; // RGB

		return sprintf($format, ...$values);
	}

	public function dump(string|null $file = null):string{

		$eps = implode("\n", [
			// initialize header
			$this->header(),
			// create the path elements
			$this->paths(),
			// end file
			'%%EOF',
		]);

		$this->saveToFile($eps, $file);

		return $eps;
	}

	/**
	 * Returns the main header for the EPS file, including function definitions and background
	 */
	protected function header():string{
		[$width, $height] = $this->getOutputDimensions();

		$header = [
			// main header
			'%!PS-Adobe-3.0 EPSF-3.0',
			'%%Creator: php-qrcode (https://github.com/chillerlan/php-qrcode)',
			'%%Title: QR Code',
			sprintf('%%%%CreationDate: %1$s', date('c')),
			'%%DocumentData: Clean7Bit',
			'%%LanguageLevel: 3',
			sprintf('%%%%BoundingBox: 0 0 %s %s', $width, $height),
			'%%EndComments',
			// function definitions
			'%%BeginProlog',
			'/F { rectfill } def',
			'/R { setrgbcolor } def',
			'/C { setcmykcolor } def',
			'%%EndProlog',
		];

		if($this::moduleValueIsValid($this->options->bgColor)){
			$header[] = $this->prepareModuleValue($this->options->bgColor);
			$header[] = sprintf('0 0 %s %s F', $width, $height);
		}

		return implode("\n", $header);
	}

	/**
	 * returns one or more EPS path blocks
	 */
	protected function paths():string{
		$paths = $this->collectModules();
		$eps   = [];

		foreach($paths as $M_TYPE => $path){

			if($path === []){
				continue;
			}

			$eps[] = $this->getModuleValue($M_TYPE);
			$eps[] = implode("\n", $path);
		}

		return implode("\n", $eps);
	}

	/**
	 * Returns a path segment for a single module
	 */
	protected function moduleTransform(int $x, int $y, int $M_TYPE, int $M_TYPE_LAYER):string|null{

		if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
			return null;
		}

		$outputX = ($x * $this->scale);
		// Actual size - one block = Topmost y pos.
		$top     = ($this->length - $this->scale);
		// Apparently y-axis is inverted (y0 is at bottom and not top) in EPS, so we have to switch the y-axis here
		$outputY = ($top - ($y * $this->scale));

		return sprintf('%d %d %d %d F', $outputX, $outputY, $this->scale, $this->scale);
	}

}
