Source: QROptions.js

/**
 * @created      11.07.2022
 * @author       smiley <smiley@chillerlan.net>
 * @copyright    2022 smiley
 * @license      MIT
 */

import QRCodeException from './QRCodeException.js';
import PHPJS from './Common/PHPJS.js';
import {
	ECC_H, ECC_L, ECC_M, ECC_Q, MASK_PATTERN_AUTO, VERSION_AUTO,
} from './Common/constants.js';
import QRMarkupSVG from './Output/QRMarkupSVG.js';

/**
 * The QRCode plug-in settings & setter functionality
 */
export default class QROptions{

	/**
	 * QR Code version number
	 *
	 * [1 ... 40] or QRCode.VERSION_AUTO
	 *
	 * @type {number|int}
	 * @protected
	 */
	_version = VERSION_AUTO;

	/**
	 * Minimum QR version
	 *
	 * if $version = QRCode.VERSION_AUTO
	 *
	 * @type {number|int}
	 * @protected
	 */
	_versionMin = 1;

	/**
	 * Maximum QR version
	 *
	 * @type {number|int}
	 * @protected
	 */
	_versionMax = 40;

	/**
	 * Error correct level
	 *
	 * QRCode::ECC_X where X is:
	 *
	 *   - L =>  7%
	 *   - M => 15%
	 *   - Q => 25%
	 *   - H => 30%
	 *
	 * @type {number|int}
	 * @protected
	 */
	_eccLevel = ECC_L;

	/**
	 * Mask Pattern to use (no value in using, mostly for unit testing purposes)
	 *
	 * [0...7] or QRCode::MASK_PATTERN_AUTO
	 *
	 * @type {number|int}
	 * @protected
	 */
	_maskPattern = MASK_PATTERN_AUTO;

	/**
	 * Add a "quiet zone" (margin) according to the QR code spec
	 *
	 * @see https://www.qrcode.com/en/howto/code.html
	 *
	 * @type {boolean}
	 */
	addQuietzone = true;

	/**
	 * Size of the quiet zone
	 *
	 * internally clamped to [0 ... $moduleCount / 2], defaults to 4 modules
	 *
	 * @type {number|int}
	 * @protected
	 */
	_quietzoneSize = 4;

	/**
	 * the FQCN of the custom QROutputInterface if $outputType is set to QRCode::OUTPUT_CUSTOM
	 *
	 * @type {string|null}
	 */
	outputInterface = QRMarkupSVG;

	/**
	 * /path/to/cache.file
	 *
	 * @type {string|null}
	 */
	cachefile = null;

	/**
	 * newline string [HTML, SVG, TEXT]
	 *
	 * @type {string}
	 */
	eol = '\n';

	/**
	 * size of a QR code module in pixels [SVG, IMAGE_*], HTML via CSS
	 *
	 * @type {number|int}
	 */
	scale = 5;

	/**
	 * a common css class
	 *
	 * @type {string}
	 */
	cssClass = 'qrcode';

	/**
	 * SVG opacity
	 *
	 * @type {number|float}
	 */
	svgOpacity = 1.0;

	/**
	 * anything between <defs>
	 *
	 * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs
	 *
	 * @type {string}
	 */
	svgDefs = '';

	/**
	 * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio
	 *
	 * @type {string}
	 */
	svgPreserveAspectRatio = 'xMidYMid';

	/**
	 * Whether to add an XML header line or not, e.g. to embed the SVG directly in HTML
	 *
	 * `<?xml version="1.0" encoding="UTF-8"?>`
	 *
	 * @type {boolean}
	 */
	svgAddXmlHeader = false;

	/**
	 * Whether to use the SVG `fill` attributes
	 *
	 * If set to `true` (default), the `fill` attribute will be set with the module value for the `<path>` element's `$M_TYPE`.
	 * When set to `false`, the module values map will be ignored and the QR Code may be styled via CSS.
	 *
	 * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
	 *
	 * @type {boolean}
	 */
	svgUseFillAttributes = true;

	/**
	 * whether to connect the paths for the several module types to avoid weird glitches when using gradients etc.
	 *
	 * @see https://github.com/chillerlan/php-qrcode/issues/57
	 *
	 * @type {boolean}
	 */
	connectPaths = false;

	/**
	 * specify which paths/patterns to exclude from connecting if $svgConnectPaths is set to true
	 *
	 * @type {number[]|int[]}
	 */
	excludeFromConnect = [];

	/**
	 * specify whether to draw the modules as filled circles
	 *
	 * a note for GDImage output:
	 *
	 * if QROptions::$scale is less or equal than 20, the image will be upscaled internally, then the modules will be drawn
	 * using imagefilledellipse() and then scaled back to the expected size using IMG_BICUBIC which in turn produces
	 * unexpected outcomes in combination with transparency - to avoid this, set scale to a value greater than 20.
	 *
	 * @see https://github.com/chillerlan/php-qrcode/issues/23
	 * @see https://github.com/chillerlan/php-qrcode/discussions/122
	 *
	 * @type {boolean}
	 */
	drawCircularModules = false;

	/**
	 * specifies the radius of the modules when $svgDrawCircularModules is set to true
	 *
	 * @type {number|float}
	 * @protected
	 */
	_circleRadius = 0.45;

	/**
	 * specifies which module types to exclude when $svgDrawCircularModules is set to true
	 *
	 * @type {number[]|int[]}
	 */
	keepAsSquare = [];

	/**
	 * toggle base64 or raw image data
	 *
	 * @type {boolean}
	 */
	outputBase64 = true;

	/**
	 * toggle background transparency
	 *
	 * if transparency is disabled, a background color should be specified to avoid unexpected outcomes
	 *
	 * @see QROptions.bgcolor
	 *
	 * @type {boolean}
	 */
	imageTransparent = true;

	/**
	 * whether to draw the light (false) modules
	 *
	 * @type {boolean}
	 */
	drawLightModules = true;

	/**
	 * Module values map
	 *
	 *   - HTML, IMAGICK: #ABCDEF, cssname, rgb(), rgba()...
	 *   - IMAGE: [63, 127, 255] // R, G, B
	 *
	 * @type {Object.<number, *>|null}
	 */
	moduleValues = null;

	/**
	 * Toggles logo space creation
	 *
	 * @type {boolean}
	 */
	addLogoSpace = false;

	/**
	 * width of the logo space
	 *
	 * @type {number|int|null}
	 * @protected
	 */
	_logoSpaceWidth = null;

	/**
	 * height of the logo space
	 *
	 * @type {number|int|null}
	 * @protected
	 */
	_logoSpaceHeight = null;

	/**
	 * optional horizontal start position of the logo space (top left corner)
	 *
	 * @type {number|int|null}
	 * @protected
	 */
	_logoSpaceStartX = null;

	/**
	 * optional vertical start position of the logo space (top left corner)
	 *
	 * @type {number|int|null}
	 * @protected
	 */
	_logoSpaceStartY = null;

	/**
	 * whether to return the markup as DOM element
	 *
	 * @type {boolean}
	 */
	returnAsDomElement = true;

	/**
	 * background color
	 *
	 * supported in:
	 *
	 * - OUTPUT_CANVAS
	 *
	 * @type {*|null}
	 */
	bgcolor = null;

	/**
	 * the canvas HTML element (canvas output only)
	 *
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement
	 *
	 * @type {HTMLCanvasElement}
	 */
	canvasElement = null;

	/**
	 * canvas image mime type for bas64/file output
	 *
	 * the value may be one of the following (depends on browser/engine):
	 *
	 * - png
	 * - jpeg
	 * - bmp
	 * - webp
	 *
	 * the "image/" is prepended internally
	 *
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL
	 *
	 * @type {string}
	 * @protected
	 */
	_canvasMimeType = 'image/png';

	/**
	 * canvas image quality
	 *
	 *  "A number between 0 and 1 indicating the image quality to be used when creating images
	 *   using file formats that support lossy compression (such as image/jpeg or image/webp).
	 *   A user agent will use its default quality value if this option is not specified,
	 *   or if the number is outside the allowed range."
	 *
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL
	 *
	 * @type {number|float}
	 */
	canvasImageQuality = 0.85;

	/**
	 * because javascript is dumb, and we can't call getters and setters directly we have to do this silly workaround.
	 * if your inherited options class uses magic getters and setters, add the relevant property names to this array
	 * and call _fromIterable() afterwards:
	 *
	 *     constructor($options = null){
	 *         super();
	 *         this.__workaround__.push('myMagicProp');
	 *         this._fromIterable($options)
	 *     }
	 *
	 *
	 *     let o = new MyExtendedOptions({myMagicProp: 'foo', ...});
	 *
	 * note; for some reason we need to add the constructor with a parent call in extended classes even without
	 * the aforementioned workaround, otherwise the additional properties will not be recognized. wtfjs???
	 *
	 * @protected
	 */
	__workaround__ = [
		'canvasMimeType',
		'circleRadius',
		'eccLevel',
		'logoSpaceHeight',
		'logoSpaceWidth',
		'logoSpaceStartX',
		'logoSpaceStartY',
		'maskPattern',
		'quietzoneSize',
		'version',
		'versionMin',
		'versionMax',
	];

	/**
	 * @param {Object<{}>|null} $options
	 */
	constructor($options = null){
		this._fromIterable($options);
	}

	/**
	 * @param {Object<{}>} $options
	 * @returns {void}
	 * @protected
	 */
	_fromIterable($options){

		if(Object.prototype.toString.call($options) !== '[object Object]'){
			return;
		}

		Object.keys($options).forEach($property => {
			if(this.__workaround__.includes($property)){
				this['_set_'+$property]($options[$property]);
			}
			// since Object.prototype.hasOwnProperty.call(this, $property) will cause issues with extended classes,
			// we'll just check if the property is defined. have i mentioned yet how much i loathe javascript?
			else if(this[$property] !== undefined){
				this[$property] = $options[$property];
			}
		});

	}

	/**
	 * clamp min/max version number
	 *
	 * @param {number|int} $versionMin
	 * @param {number|int} $versionMax
	 *
	 * @returns {void}
	 *
	 * @protected
	 */
	setMinMaxVersion($versionMin, $versionMax){
		let $min = Math.max(1, Math.min(40, $versionMin));
		let $max = Math.max(1, Math.min(40, $versionMax));

		this._versionMin = Math.min($min, $max);
		this._versionMax = Math.max($min, $max);
	}

	/**
	 * sets the minimum version number
	 *
	 * @param {number|int} $versionMin
	 *
	 * @returns {void}
	 * @protected
	 */
	_set_versionMin($versionMin){
		this.setMinMaxVersion($versionMin, this._versionMax);
	}

	set versionMin($versionMin){
		this._set_versionMin($versionMin);
	}

	get versionMin(){
		return this._versionMin;
	}

	/**
	 * sets the maximum version number
	 *
	 * @param {number|int} $versionMax
	 *
	 * @returns {void}
	 * @protected
	 */
	_set_versionMax($versionMax){
		this.setMinMaxVersion(this._versionMin, $versionMax);
	}

	set versionMax($versionMax){
		this._set_versionMax($versionMax);
	}

	get versionMax(){
		return this._versionMax;
	}

	/**
	 * sets/clamps the version number
	 *
	 * @param {number|int} $version
	 *
	 * @returns {void}
	 * @protected
	 */
	_set_version($version){
		this._version = $version !== VERSION_AUTO ? Math.max(1, Math.min(40, $version)) : VERSION_AUTO;
	}

	set version($version){
		this._set_version($version);
	}

	get version(){
		return this._version;
	}

	/**
	 * sets the error correction level
	 *
	 * @param {number|int} $eccLevel
	 *
	 * @returns {void}
	 * @throws QRCodeException
	 * @protected
	 */
	_set_eccLevel($eccLevel){

		if(![ECC_L, ECC_M, ECC_Q, ECC_H].includes($eccLevel)){
			throw new QRCodeException(`Invalid error correct level: ${$eccLevel}`);
		}

		this._eccLevel = $eccLevel;
	}

	set eccLevel($eccLevel){
		this._set_eccLevel($eccLevel);
	}

	get eccLevel(){
		return this._eccLevel;
	}

	/**
	 * sets/clamps the mask pattern
	 *
	 * @param {number|int} $maskPattern
	 *
	 * @returns {void}
	 * @protected
	 */
	_set_maskPattern($maskPattern){

		if($maskPattern !== MASK_PATTERN_AUTO){
			this._maskPattern = Math.max(0, Math.min(7, $maskPattern));
		}

	}

	set maskPattern($maskPattern){
		this._set_maskPattern($maskPattern);
	}

	get maskPattern(){
		return this._maskPattern;
	}

	/**
	 * sets/clamps the quiet zone size
	 *
	 * @param {number|int} $quietzoneSize
	 *
	 * @returns {void}
	 * @protected
	 */
	_set_quietzoneSize($quietzoneSize){
		this._quietzoneSize = Math.max(0, Math.min($quietzoneSize, 75));
	}

	set quietzoneSize($quietzoneSize){
		this._set_quietzoneSize($quietzoneSize) ;
	}

	get quietzoneSize(){
		return this._quietzoneSize;
	}

	/**
	 * clamp the logo space values between 0 and maximum length (177 modules at version 40)
	 *
	 * @param {number|int} $value
	 *
	 * @returns {number|int}
	 * @protected
	 */
	clampLogoSpaceValue($value){
		$value = PHPJS.intval($value);

		return Math.max(0, Math.min(177, $value));
	}

	/**
	 * clamp/set logo space width
	 *
	 * @param {number|int} $width
	 *
	 * @returns {void}
	 * @protected
	 */
	_set_logoSpaceWidth($width){
		this._logoSpaceWidth = this.clampLogoSpaceValue($width);
	}

	set logoSpaceWidth($width){
		this._set_logoSpaceWidth($width);
	}

	get logoSpaceWidth(){
		return this._logoSpaceWidth;
	}

	/**
	 * clamp/set logo space height
	 *
	 * @param {number|int} $height
	 *
	 * @returns {void}
	 * @protected
	 */
	_set_logoSpaceHeight($height){
		this._logoSpaceHeight = this.clampLogoSpaceValue($height);
	}

	set logoSpaceHeight($height){
		this._set_logoSpaceHeight($height);
	}

	get logoSpaceHeight(){
		return this._logoSpaceHeight;
	}

	/**
	 * clamp/set horizontal logo space start
	 *
	 * @param {number|int|null} $startX
	 *
	 * @returns {void}
	 * @protected
	 */
	_set_logoSpaceStartX($startX){
		this._logoSpaceStartX = (typeof $startX === 'undefined' || $startX === null) ? null : this.clampLogoSpaceValue($startX);
	}

	set logoSpaceStartX($startX){
		this._set_logoSpaceStartX($startX);
	}

	get logoSpaceStartX(){
		return this._logoSpaceStartX;
	}

	/**
	 * clamp/set vertical logo space start
	 *
	 * @param {number|int|null} $startY
	 *
	 * @returns {void}
	 * @protected
	 */
	_set_logoSpaceStartY($startY){
		this._logoSpaceStartY = (typeof $startY === 'undefined' || $startY === null) ? null : this.clampLogoSpaceValue($startY);
	}

	set logoSpaceStartY($startY){
		this._set_logoSpaceStartY($startY);
	}

	get logoSpaceStartY(){
		return this._logoSpaceStartY;
	}

	/**
	 * clamp/set SVG circle radius
	 *
	 * @param {number|float} $circleRadius
	 *
	 * @returns {void}
	 * @protected
	 */
	_set_circleRadius($circleRadius){
		this._circleRadius = Math.max(0.1, Math.min(0.75, $circleRadius));
	}

	set circleRadius($circleRadius){
		this._set_circleRadius($circleRadius);
	}

	get circleRadius(){
		return this._circleRadius;
	}

	/**
	 * set canvas image type
	 *
	 * @param {string} $canvasImageType
	 *
	 * @returns {void}
	 * @protected
	 */
	_set_canvasMimeType($canvasImageType){
		$canvasImageType = $canvasImageType.toLowerCase();

		if(!['bmp', 'jpeg', 'png', 'webp'].includes($canvasImageType)){
			throw new QRCodeException(`Invalid canvas image type: ${$canvasImageType}`);
		}

		this._canvasMimeType = `image/${$canvasImageType}`;
	}

	set canvasMimeType($canvasImageType){
		this._set_canvasMimeType($canvasImageType);
	}

	get canvasMimeType(){
		return this._canvasMimeType;
	}

}