<?php

namespace App\Config;

/**
 * The class is used for managing/parsing remarks
 */
class Remarks extends XmlDynamicRecordsAbstract
{
	/**
	 * Stores the configuration file (relative to the "files/config/" dir). For config files stored
	 * inside subdirectories, the subdirectory is included (Ex: services/servicesList.xml).
	 * @var String
	 */
	protected $fileName=CONFIG_FILE_REMARKS;

	private $selectedCategory='service';

	/**
	 * Create the admin interface
	 */
	protected function constructAdmin()
	{

		parent::constructAdmin();

		global $S;
		$className=get_called_class();

		// selected category
		if (isset($S['admin']['configApp'][$className]['selectedCategory'])) {
			$this->selectedCategory=$S['admin']['configApp'][$className]['selectedCategory'];
		}
		$S['admin']['configApp'][$className]['selectedCategory']=&$this->selectedCategory;
	}

	/**
	 * Resets the details stored in session. I rewrote this method because must to be reset and itemsListComplete and selectedCategory form session.
	 */
	public function reset()
	{
		$this->selectedCategory='service';
		parent::reset();
	}

	protected function doItemInsert($code, $itemDetails)
	{

		$itemDetails['category'] = $this->selectedCategory;
		$itemDetails['idAddUser'] = CURRENT_ID_USER;
		$itemDetails['addTime'] = \Engine\DateTime::create()->formatDatetimeIso();
		$itemDetails['id'] = $this->selectedCategory.'|'.$code;
		parent::doItemInsert($code, $itemDetails);
	}

	protected function doItemUpdate($code, $itemDetails)
	{
		$itemDetails['category'] = $this->selectedCategory;
		$itemDetails['idUpdateUser'] = CURRENT_ID_USER;
		$itemDetails['lastUpdateTime'] = \Engine\DateTime::create()->formatDatetimeIso();
		$itemDetails['id'] = $this->selectedCategory.'|'.$code;
		parent::doItemUpdate($code, $itemDetails);
	}

	/**
	 * This method returns selected category from session.
	 */
	public function getHandledCategory()
	{
		// check constructor type
		if ($this->constructType!='admin') {
			throw new \Exception('You can call '.__METHOD__.' only for `admin` constructor.');
		}
		return $this->selectedCategory;
	}


	/**
	 * This method set itemsList from session for selected category
	 */
	public function setHandledCategory($category)
	{
		// check constructor type
		if ($this->constructType!='admin') {
			throw new \Exception('You can call '.__METHOD__.' only for `admin` constructor.');
		}

		$this->selectedCategory = $category;
		return $this;
	}

	/**
	 * Gets an array that contains remark types
	 * @return array
	 */
	public function getValidRemarkTypes()
	{
		return ['service', 'contactPerson', 'corporate', 'corporateOffice', 'customer', 'office', 'supplier', 'supplierService', 'user', 'invoice', 'invoiceCashing', 'supplierInvoice', 'supplierInvoicePayment', 'amadeus', 'sabre', 'galileo', 'apollo', 'worldspan', 'miceProject', 'offerProfileRule', 'balance'];
	}

	/**
	 * Gets an array that contains vars types
	 * @return array
	 */
	public function getValidVarsTypes()
	{

		// standard types
		$typesList= ['int', 'float', 'digits', 'alnum', 'string', 'date', 'currency', 'boolean', 'select', 'blank'];

		return $typesList;
	}

	/**
	 * Return an array with valid autocomplete types
	 * @return array
	 */
	public function getValidRemarkVarAutocompleteTypes()
	{
		return ['supplier', 'user', 'customer', 'corporate', 'office', 'city', 'documentSeries'];
	}


	/**
	 * Gets a list with all categories
	 * @see \App\Config\XmlDynamicRecordsAbstract::getAll_rawData_extractDataFromFile_list()
	 */
	protected function getAll_rawData_extractDataFromFile_list(\SimpleXMLElement $xml)
	{

		// get a remark list from this category
		$remarksList = [];

		// SCAN EACH REMARKS CATEGORY //////////////////////////////////////////////////////////////
		if ($xml->remarksCategory) {
			foreach($xml->remarksCategory as $remarksCategoryDetails) {

				// type of remarks category
				$rmCategory=(string)$remarksCategoryDetails['type'];

				// SCAN EACH REMARK FROM THIS CATEGORY /////////////////////////////////////////////////
				foreach ($remarksCategoryDetails as $thisRm) {

					// create a new remark as an array
					$newRm=[];

					// remark format
					$newRm['format']=(String)$thisRm->format;

					// remark category
					$newRm['category']= $rmCategory;

					//variables of remark
					$newRm['vars']=[];

					// visibility conditions
					$newRm['visibilityConditions'] = [];

					// set up visibility conditions
					if(is_object($thisRm->visibilityConditions->condition)) {
						$conditions = $thisRm->visibilityConditions->condition;
						foreach($conditions as $condition) {
							$newRm['visibilityConditions'][] = [ 'field' => (String) $condition['field'], 'value' => (String) $condition['value']];
						}
					}

					$hasAssistedInputs = false;

					// SCAN EACH VARIABLE AND STORE DETAILS IN $rmVar //////////////////////////////////
					foreach($thisRm->var as $var) {

						// create an array that contains name, type
						$rmVar=[];

						// ADD TO THIS ARRAY NAME, TYPE AND ASSISTEDINPUT ------------------------------

						// add name of this variable (is mandatory)
						$rmVar['name'] = (String) $var['name'];

						// add type of this variable (is mandatory)
						$rmVar['type'] = (String) $var['type'];

						// add assisted input of this variable ( true if attribute assisted input was writen)
						$rmVar['assistedInput'] = (Bool)(String) $var['assistedInput'];

						// CHECK DEFAULT, OPTIONAL, MINLENGTH, MAXLENGTH, FIXLENGTH AND ADD TO VAR ARRAY
						if ((String)$var['default']) {
							$rmVar['default']=(String)$var['default'];
						}
						if ((String)$var['optional']) {
							$rmVar['optional']=true;
						}
						if ((Int)$var['minLength']) {
							$rmVar['minLength']=(Int)$var['minLength'];
						}
						if ((Int)$var['maxLength']) {
							$rmVar['maxLength']=(Int)$var['maxLength'];
						}
						if ((Int)$var['fixLength']) {
							$rmVar['fixLength']=(Int)$var['fixLength'];
						}

						// LOAD THE SELECTOR OPTIONS IF TYPE IS SELECT ---------------------------------

						if ((String)$var['type']=="select") {

							$optionsList=$var->selectOptions->option;

							// scan each option
							foreach($optionsList as $opt) {

								$rmVar['options'][]=(String)$opt;
							}
						}

						// LOAD THE ASSISTED INPUT DETAILS ---------------------------------------------

						if ($rmVar['assistedInput']) {

							$hasAssistedInputs = true;

							$rmVar['assistedInputDetails']=[];

							// add category for assisted input
							$rmVar['assistedInputDetails']['category']=(String)$var->assistedInput->category;

							// add name  for assisted input
							$rmVar['assistedInputDetails']['name']=(String)$var->assistedInput->name;

							// add description for assisted input
							$rmVar['assistedInputDetails']['description']=(String)$var->assistedInput->description;

							// add multiLine checkbox
							if ((Int)$var->assistedInput['multiLine']) {
								$rmVar['assistedInputDetails']['multiLine'] = true;
							}

							// add autocomplete for assisted input
							$rmVar['assistedInputDetails']['autocompleteType']=(String)$var->assistedInput->autocompleteType;

							// display select as radio
							if (isset($var->assistedInput['displaySelectAsRadio'])) {
								$rmVar['assistedInputDetails']['displaySelectAsRadio']=(Int)$var->assistedInput['displaySelectAsRadio'];
							}

							// check select options
							if (isset($var->assistedInput->selectOptions)) {
								$rmVar['assistedInputDetails']['selectOptions']=[];

								// store selected options values (translations)
								foreach($var->assistedInput->selectOptions->option as $opt) {
									$rmVar['assistedInputDetails']['selectOptions'][(String)$opt['value']]=(String)$opt;
								}
							}
						}

						$newRm['vars'][]=$rmVar;

					}

					// set if the vars have assisted inputs
					$newRm['hasAssistedInputs'] = $hasAssistedInputs;

					// gets the regular expression for this format
					$newRm['regExp'] = $this->getRegexp($newRm['format'], $newRm['vars']);

					// check comments and add to new comments
					if ((String)$thisRm->comments) {
						$newRm['comments'] = (String)$thisRm->comments;
					}

					$newRm['addTime'] = (String)$thisRm['addTime'];
					$newRm['idAddUser'] = (String)$thisRm['idAddUser'];
					$newRm['lastUpdateTime'] = (String)$thisRm['lastUpdateTime'];
					$newRm['idUpdateUser'] = (String)$thisRm['idUpdateUser'];

					$key = uniqid();
					$newRm['id'] = $rmCategory.'|'.$key;	// TODO: salveaza id-urile in XML, pentru viteza, renunta la uniqid

					// for this category add new remark
					$remarksList[$key] = $newRm;
				}

			}

		}

		// add in array  new remark types
		return $remarksList;
	}


	/**
	 * (non-PHPdoc)
	 * @see \App\Config\XmlDynamicRecordsAbstract::getAll_rawData_extractDataFromFile_default()
	 */
	protected function getAll_rawData_extractDataFromFile_default(\SimpleXMLElement $xml)
	{
		return '';
	}

	/**
	 * (non-PHPdoc)
	 * @see \App\Config\XmlDynamicRecordsAbstract::getAll_rawData_extractDataFromFile_mandatory()
	 */
	protected function getAll_rawData_extractDataFromFile_mandatory(\SimpleXMLElement $xml)
	{
		return true;
	}

	/**
	 * (non-PHPdoc)
	 * @see \App\Config\XmlDynamicRecordsAbstract::itemCheck()
	 */
	protected function itemCheck($code, $itemDetails)
	{

		// check itemDetails
		if (!is_array($itemDetails)) {
			throw new \Exception('Remark details must be set as an array.');
		}

		// check code to be a numeric value
		if (!strlen($code)) {
			$this->addErrorMessage(sprintf('The `remark code` was not defined.'));
		}

		// check format
		if (!$itemDetails['format']) {
			$this->addErrorMessage(sprintf('The `remark format` was not defined.'));
		}

		// check if the format is already used for the remark category
		if ($itemDetails['format'] and $this->getAll()) {
			foreach ($this->getAll() as $id=>$item) {
				if ($id!=$code and $item['category']==$this->selectedCategory and $itemDetails['format']==$item['format']) {
					$this->addErrorMessage(sprintf('The remark format `%s` is already defined.', $item['format']));
				}
			}
		}

		$varNamesList = $this->parseRemarkFormat($itemDetails['format']);


		if ($itemDetails['visibilityConditions'] && !is_array($itemDetails['visibilityConditions'])) {
			$this->addErrorMessage(sprintf('The `remark visibility conditions` were not defined as array.'));
		}

		// check vars to be an array and to contains min one char
		if (!(is_array($itemDetails['vars']) and count($itemDetails['vars']))) {
			$this->addErrorMessage(sprintf('The `remark` must contains at least one variable.'));
		}

		// scan each variable form vars list
		foreach ($itemDetails['vars'] as $varIndex => $varDetails) {

			// check the var name
			if (!$varDetails['name']){
				$this->addErrorMessage(sprintf('This `variable name` is not defined. '));
			}


			// check the name of var from vars to correspond with name of var form format
			if ($varNamesList[$varIndex]!=$varDetails['name']) {
				$this->addErrorMessage(sprintf("The variable name `%s` does not correspond to name `%s` from remark format", $varDetails['name'], $varNamesList[$varIndex]));
			}

			// check the numbers at the beginning of name
			if (preg_match('/^[1-9]/', $varDetails['name'])) {
				$this->addErrorMessage(sprintf("The name of variable contains the digits at the beginning of name"));
			}

			// check special chars
			if (!preg_match('/^[a-zA-Z0-9\_]+$/', $varDetails['name'])) {
				$this->addErrorMessage(sprintf("The name of variable '%s' contains special characters. ", $varDetails['name']));
			}

			// check vars length
			if (mb_strlen((string)$varDetails['name'])>50) {
				$this->addErrorMessage(sprintf("Variable name length can not be more than 50 characters"));
			}

			// check that the var type is in the allowed var type
			if (!in_array($varDetails['type'], $this->getValidVarsTypes())){
				$this->addErrorMessage(sprintf('Type `%s` for this remark is not valid.', $varDetails['type']));
			}

			// check to be a min length and a max length or a fix length
			if (($varDetails['minLength'] or $varDetails['maxLength']) and $varDetails['fixLength']) {
				$this->addErrorMessage(sprintf('Can not be set fixed length and minimum or maximum length'));
			}

			// check minLength if this is set
			if (strlen((string)$varDetails['minLength'])>0 and !\Engine\Validator::set($varDetails['minLength'], ['type' => 'int', 'minValue' => 0, 'maxValue' => 1000])->check() ) {
				$this->addErrorMessage(sprintf("The `min length` must be a numeric value between 0 and 1000. "));
			}

			// check minLength if this is set
			if (strlen((string)$varDetails['maxLength'])>0 and !\Engine\Validator::set($varDetails['maxLength'], ['type' => 'int', 'minValue' => 0, 'maxValue' => 1000])->check()) {
				$this->addErrorMessage(sprintf("The `max length` must be a numeric value between 0 and 1000. "));
			}

			// check fixLength is this is set
			if (strlen((string)$varDetails['fixLength'])>0 and !\Engine\Validator::set($varDetails['fixLength'], ['type' => 'int', 'minValue' => 0, 'maxValue' => 1000])->check()) {
				$this->addErrorMessage(sprintf("The `fix length` must be a numeric value between 0 and 1000. "));
			}

			// check options in select
			if ($varDetails['type']=='select') {
				if (!is_array($varDetails['options']) and !count($varDetails['options'])) {
					$this->addErrorMessage(sprintf('Format for options list is not valid. '));
				}
				// no options on select
				if (!$varDetails['options']) {
					$this->addErrorMessage(sprintf('There are no options available for selection list. '));
				}
				foreach ($varDetails['options'] as $optionDet) {
					if (preg_match("/[\&\>\<]/", $optionDet)) {
						$this->addErrorMessage(sprintf('There is a value in the remark var %s that contains special chars - `%s`.', $varDetails['name'], $optionDet));
					}
				}
			}

			//'digits', 'int', 'float', 'alnum', 'string', 'blank', 'currency', 'select', 'date', 'boolean'
			// check default value to be included in the selected type
			if ($varDetails['default']) {

				switch ($varDetails['type']) {

					// for 'digits', 'int', 'float', 'alnum', 'string' we may have a length
					case 'digits':
					case 'alnum':
					case 'string' : {

						if ($varDetails['minLength'] and $varDetails['maxLength']) {
							$arrayCheck= ['type' => $varDetails['type'], 'minLength' => $varDetails['minLength'], 'maxLength' => $varDetails['maxLength']];
						}
						elseif ($varDetails['minLength']) {
							$arrayCheck= ['type' => $varDetails['type'], 'minLength' => $varDetails['minLength']];
						}
						elseif ($varDetails['maxLength']) {
							$arrayCheck= ['type' => $varDetails['type'], 'maxLength' => $varDetails['maxLength']];
						}
						elseif ($varDetails['fixLength']) {
							$arrayCheck= ['type' => $varDetails['type'], 'fixLength' => $varDetails['fixLength']];
						}
						else {
							$arrayCheck= ['type' => $varDetails['type']];
						}

						if (!\Engine\Validator::set($varDetails['default'], $arrayCheck)->check()) {
							$this->addErrorMessage(sprintf('Default value `%s` does not match the selected type `%s` for remark `%s`. ', $varDetails['default'], $varDetails['type'], $itemDetails['format']));
						}
						break;
					}

					case 'date' : {
						if (!\Engine\Validator::set($varDetails['default'], ['type'=>'date'])->check()) {
							$this->addErrorMessage(sprintf('Default value `%s` does not match the selected type `%s` for remark `%s`. ', $varDetails['default'], $varDetails['type'], $itemDetails['format']));
						}
						break;
					}

					case 'currency' : {
						if (!Currencies::getInstance()->check($varDetails['default'])) {
							$this->addErrorMessage(sprintf('Default value `%s` must be a valid currency for remark `%s`', $varDetails['default'], $itemDetails['format']));
						}
						break;
					}
				}
			}
		}
	}


	/**
	 * (non-PHPdoc)
	 * @see \App\Config\BaseAbstract::save_check()
	 */
	protected function save_check()
	{
	}

	/**
	 * (non-PHPdoc)
	 * @see \App\Config\XmlBasicAbstract::save_write_do_getXml()
	 */
	protected function save_write_do_getXml()
	{

		$xml=simplexml_load_string('<?xml version="1.0" encoding="UTF-8"?><remarks />');
		// return the xml content of the current object

		foreach ($this->getValidRemarkTypes() as $category) {
			// add obj remarks category
			$categoryXmlObj = $xml->addChild('remarksCategory');

			// add attr category type
			$categoryXmlObj['type']=$category;
		}


		foreach ($this->itemsList as $remarkDetails) {

			foreach ($xml->remarksCategory as $remarkCategoryTag) {
				if ((String)$remarkCategoryTag['type']==$remarkDetails['category']) {
					$remarkXmlObj = $remarkCategoryTag->addChild('remark');
				}
			}

			// add insert/update user and time data
			if ($remarkDetails['idAddUser']) {
				$remarkXmlObj['idAddUser'] = $remarkDetails['idAddUser'];
			}

			if ($remarkDetails['addTime']) {
				$remarkXmlObj['addTime'] = $remarkDetails['addTime'];
			}

			if ($remarkDetails['idUpdateUser']) {
				$remarkXmlObj['idUpdateUser'] = $remarkDetails['idUpdateUser'];
			}

			if ($remarkDetails['lastUpdateTime']) {
				$remarkXmlObj['lastUpdateTime'] = $remarkDetails['lastUpdateTime'];
			}


			// add format for this remark
			$remarkXmlObj->addChild('format', $remarkDetails['format']);

			// add var for this remark
			foreach ($remarkDetails['vars'] as $varsDetails) {

				// add tag var
				$varXmlObj = $remarkXmlObj->addChild('var');

				// add attr name
				$varXmlObj['name']=$varsDetails['name'];

				// add attr type
				$varXmlObj['type']=$varsDetails['type'];

				// cheack assisted input and if that is true add attr assisted input
				if ((Bool)$varsDetails['assistedInput']) {
					$varXmlObj['assistedInput']=(Bool)$varsDetails['assistedInput'];
				}

				if(in_array($varsDetails['type'], ['digits','alnum','string'])) {

					// add attr minLength
					if ($varsDetails['minLength']) {
						$varXmlObj['minLength']=$varsDetails['minLength'];
					}

					// add attr maxLength
					if ($varsDetails['maxLength']) {
						$varXmlObj['maxLength']=$varsDetails['maxLength'];
					}

					// add attr fixLength
					if ($varsDetails['fixLength']) {
						$varXmlObj['fixLength']=$varsDetails['fixLength'];
					}
				}

				// selectOptions ---------------------------------------------------------------
				if ($varsDetails['type'] == 'select') {
					$varXmlOptions=$varXmlObj->addChild('selectOptions');

					foreach ($varsDetails['options'] as  $option) {

						// option details is an array ('option')
						if (strlen($option)) {
							$varXmlOptions->addChild('option', $option);
						}
					}
				}


				// assisted input ------------------------------------------------------------------

				if ($varsDetails['assistedInput']) {


					// create xml obj assistedInput
					$varXmlAssistedInput=$varXmlObj->addChild('assistedInput');

					// display select as radio
					if (isset($varsDetails['assistedInputDetails']['displaySelectAsRadio'])) {
						$varXmlAssistedInput['displaySelectAsRadio']=$varsDetails['assistedInputDetails']['displaySelectAsRadio'];
					}

					// multiLine flag
					if ($varsDetails['assistedInputDetails']['multiLine']) {
						$varXmlAssistedInput['multiLine'] = 1;
					}

					// add name for assisted input
					if ($varsDetails['assistedInputDetails']['name']) {
						$varXmlAssistedInput->addChild('name', $varsDetails['assistedInputDetails']['name']);
					}

					// add category for assisted input
					if ( $varsDetails['assistedInputDetails']['category'] ){
						$varXmlAssistedInput->addChild('category', $varsDetails['assistedInputDetails']['category']);
					}

					// add description for assisted input
					if ($varsDetails['assistedInputDetails']['description']) {
						$varXmlAssistedInput->addChild('description', $varsDetails['assistedInputDetails']['description']);
					}

					// add autocompleteType for assisted input
					if ($varsDetails['assistedInputDetails']['autocompleteType']) {
						$varXmlAssistedInput->addChild('autocompleteType', $varsDetails['assistedInputDetails']['autocompleteType']);
					}

					// selected options
					if ($varsDetails['assistedInputDetails']['selectOptions']) {
						$varXmlAssistedInputSelectOption=$varXmlAssistedInput->addChild('selectOptions');

						// scan each option from selected options
						foreach ($varsDetails['assistedInputDetails']['selectOptions'] as $valOpt => $option) {
							$varXmlAssistedInputOption=$varXmlAssistedInputSelectOption->addChild('option', $option);
							$varXmlAssistedInputOption['value'] = $valOpt;
						}
					}
				}
			}

			// add visibilityConditions for this remark
			if($remarkDetails['visibilityConditions']) {
				$condsXmlObj = $remarkXmlObj->addChild('visibilityConditions');
				foreach ($remarkDetails['visibilityConditions'] as $condition) {

					// add tag condition
					$condXmlObj = $condsXmlObj->addChild('condition');
					$condXmlObj['field'] = $condition['field'];
					$condXmlObj['value'] = $condition['value'];
				}
			}

			// add comments for this remark
			if ($remarkDetails['comments']) {
				$remarkXmlObj->addChild('comments', $remarkDetails['comments']);
			}
		}


		return $xml;
	}

	/**
	 * Does the actual deletion of the element from the list.
	 * @param String $code
	 */
	protected function doItemDelete($code)
	{
		unset($this->itemsList[$code]);
	}



	/**
	 * eturns all the remarks for the category selected
	 * @param string $category
	 */
	public function getAllRemarksFromCategory($category)
	{
		// check if category is valid
		if (!in_array($category, $this->getValidRemarkTypes())) {
			throw new \Exception("Remarks type (category) is not valid for : `{$category}`");
		}

		$allRemarks = $this->getAll();

		$return = [];

		foreach ($allRemarks as $remark) {
			if ($remark['category'] == $category) {
				$return[] = $remark;
			}
		}

		return $return;

	}

	/**
	 * Indicates if a certain remark type has assisted remarks vars.
	 * @param String $type
	 * @throws \Exception - for an invalid type
	 * @return boolean
	 */
	public function remarkTypeHasAssistedRemarks($type)
	{

		// CHECK INPUTS ///////////////////////////////////////////////////////////////////////////
		// check if type is valid
		if (!in_array($type, $this->getValidRemarkTypes())) {
			throw new \Exception("Remarks type (category) is not valid for : `{$type}`");
		}

		// get the remarks of the requested type
		$currentRemarks = $this->getAllRemarksFromCategory($type);

		// go through each remark and check if at least one var it has asissted remarks
		if ($currentRemarks) {
			foreach($currentRemarks as $thisRm) {
				foreach($thisRm['vars'] as $rmVar) {
					if ($rmVar['assistedInput']) {
						return true;
					}
				}
			}
		}

		return false;

	}

	/**
	 * Indicates if a certain remark type has assisted remarks vars.
	 * @param String $type
	 * @throws \Exception - for an invalid type
	 * @return boolean
	 */
	public function remarkTypeAndServiceHasAssistedRemarks($type, $serviceType)
	{

		// CHECK INPUTS ///////////////////////////////////////////////////////////////////////////
		// check if type is valid
		if (!in_array($type, $this->getValidRemarkTypes())) {
			throw new \Exception("Remarks type (category) is not valid for : `{$type}`");
		}

		// get the remarks of the requested type
		$currentRemarks = $this->getAllRemarksFromCategory($type);

		// go through each remark and check if at least one var it has asissted remarks
		if ($currentRemarks) {
			foreach($currentRemarks as $thisRm) {
				if (empty($thisRm['visibilityConditions']) || array_filter($thisRm['visibilityConditions'], function($v) use ($serviceType){return ($v['field']=='serviceType' && $v['value']==$serviceType);})) {
					foreach($thisRm['vars'] as $rmVar) {
						if ($rmVar['assistedInput']) {
							return true;
						}
					}
				}
			}
		}

		return false;

	}


	/**
	 * Returns the regular expression for one remark.
	 * @param Array $remark
	 */
	// TODO: not used (yet)
	private function getRegexp($format, $vars)
	{
		//var_dump($vars);
		//var_dump($format);

		// hand-crafted preg_quote replacement, that makes sure we dont escape every possible character, because some of them are used by the remark parser('[', ']', '*')
		$format = str_replace(
			['/', 		'.', 	'+', 	'$', 	'^'],
			['\\/', 	'\.', 	'\+', 	'\$', 	'\^'],
			$format
		);

		// LOOP THROUGH EACH VARIABLE IN THE FORMAT ///////////////////////////////////////////////
		$replaceVars=[];
		foreach($vars as $var) {

			// DETERMINE varRegExp ////////////////////////////////////////////////////////////////
			$varRegExp='';
			switch($var['type']) {

				// CASE: digits, int, float, alnum, string ---------------------------------------------------
				case 'digits': case 'alnum': case 'string': {

					//var_dump($var['type']);
					//var_dump($format);

					$varRegExp.="(";

					if ($var['type']=='digits') $varRegExp.='[0-9]';
					elseif ($var['type']=='alnum') $varRegExp.='[0-9a-zA-Z]';
					elseif ($var['type']=='string') $varRegExp.='.';

					if ($var['fixLength']) {
						$varRegExp.='{'.$var['fixLength'].'}';
					}
					elseif ($var['minLength'] or $var['maxLength']) {
						$varRegExp.='{'.($var['minLength']?$var['minLength']:'0').','.($var['maxLength']?$var['maxLength']:'').'}';
					} else {
						$varRegExp.='*';
					}

					$varRegExp.=")";

					break;
				}

				// CASE: int ----------------------------------------------------------------------
				case 'int': {
					$varRegExp.='([\-\+]?[0-9]+)';
					break;
				}

				// CASE: float --------------------------------------------------------------------
				case 'float': {
					// ?: means non-capturing group. It will NOT extract the group in the paranthesis as a separate group
					$varRegExp .= '((?:\+|-)?(?:[0-9]+\.[0-9]+|[0-9]+|\.[0-9]+))';
					break;
				}

				// CASE: blank --------------------------------------------------------------------
				case 'blank': {
					$varRegExp.='(.*)';
					break;
				}

				// CASE: currency -----------------------------------------------------------------
				case 'currency': {
					$varRegExp.='([\p{L}]{3})';
					break;
				}

				// CASE: select -------------------------------------------------------------------
				case 'select': {
					$varRegExp.='(.*)';
					break;
				}

				// CASE: date ---------------------------------------------------------------------
				case 'date': {
					// ?: means non-capturing group. It will NOT extract the group in the paranthesis as a separate group
					$varRegExp.='((?:1|2)[0-9]{3}\-[0-9]{2}\-[0-9]{2})'; // a better regexp (probably slower: '((19|20)[0-9]{2}\-(01|02|03|04|05|06|07|08|09|10|11|12)\-(((0|1|2)[0-9])|30|31))'
					break;
				}

				// CASE 'boolean ------------------------------------------------------------------
				case 'boolean': {
					$varRegExp.='(.*)';
					break;
				}

				// A REGULAR EXPRESSION -----------------------------------------------------------
				default: {
					throw new \Exception("The remark varType `{$var['type']}` is not valid.");
				}

			}

			// add "optional" flag
			if ($var['optional']) $varRegExp.='?';

			// store the current regexp for the current variable
			$replaceVars['['.$var['name'].']']=$varRegExp;

		}

		// CREATE THE FINAL REGULAR EXPRESSION ////////////////////////////////////////////////////
		$regExp=str_replace(array_keys($replaceVars), array_values($replaceVars), $format);
		// clean the [*] from the beginning
		if (substr($regExp, 0, 3)=='[*]') $regExp=substr($regExp, 3); // can have junk before
		else $regExp='^'.$regExp; // the remark starts clean

		// clean the [*] from the end
		if (substr($regExp, -3)=='[*]')  $regExp=substr($regExp, 0, -3); // can have junk after
		else $regExp=$regExp.'$'; // the remark ends clean
		// add flags
		$regExp='/'.$regExp.'/Si';

		// replace 'any' string (added in the middle)
		$regExp=str_replace('[*]', '.*?', $regExp);

		// RETURN RESULT //////////////////////////////////////////////////////////////////////////

		return $regExp;

	}

	/**
	 *
	 * @param String $format
	 */
	public function parseRemarkFormat($format)
	{

		$vars = [];
		preg_match_all("/\[(?P<name>[a-zA-Z][a-zA-Z0-9_]*)\]/", $format, $vars);

		$varNamesList = [];
		foreach($vars['name'] as $varName){
			if ($varName != '*') {
				$varNamesList[] = $varName;
			}
		}

		return $varNamesList;
	}


	/**
	 * Clear all the remarks from session
	 */
	public function clearAll()
	{
		// check constructor type
		if ($this->constructType!='admin') throw new \Exception("You can use the method `".get_called_class().'::'.__FUNCTION__."` only if the constructor type is `admin`.");

		$this->itemsList = null;

		$this->changedFlag=true;

		return $this;
	}

	/**
	 * Add a new remark
	 * @param array $remark
	 * is used in unit tests
	 */
	public function appendRemark($remark)
	{
		// check constructor type
		if ($this->constructType!='admin') throw new \Exception("You can use the method `".get_called_class().'::'.__FUNCTION__."` only if the constructor type is `admin`.");

		$varsList = [];
		foreach ($remark['variables'] as $var) {
			$varArr = [
				'name' 		=> $var['name'],
				'type' 		=> $var['type'],
				'fixLength' => $var['fixLength'],
				'minLength' => $var['minLength'],
				'maxLength' => $var['maxLength'],
				'options' 	=> $var['options'],
			];

			$varsList[] = $varArr;
		}


		$this->itemInsert(uniqid(), [
			'format' 	=> $remark['format'],
			'vars' 		=> $varsList,
			'comments' 	=> $remark['comments']
		]);

	}


	/**
	 *
	 * @param String $remarkBulk
	 * @param String $type
	 */
	static public function process($remarkBulk, $type): array
	{
		// TODO: we could cache the output of this call, for a GDS file parsing, this gets called about 20+ times
		$detailedRemarks = self::getInstance()->getProcessedRemarksDetailedList($remarkBulk, self::getInstance()->getAllRemarksFromCategory($type));
		$processedRemarks = [];
		foreach ($detailedRemarks as $rm) {
			$processedRemarks[$rm['key']] = $rm['value'];
		}

		return $processedRemarks;
	}

	/**
	 *
	 * @param String $remarksBulk
	 * @param String $type
	 * @throws \Exception
	 */
	public function getProcessedRemarksDetailed($remarksBulk, $remarksConfig)
	{
		$detailedRemarks = self::getInstance()->getProcessedRemarksDetailedList($remarksBulk, $remarksConfig);
		$processedRemarks = [];
		foreach ($detailedRemarks as $rm) {
			$processedRemarks[$rm['key']] = $rm;
		}

		return $processedRemarks;
	}


	public function getProcessedRemarksDetailedList($remarksBulk, $remarksConfig)
	{
		// CHECK INPUTS

		// check if some remarks set
		if (!$remarksBulk) {
			return [];
		}

		// GET REMARKS CONFIGS AND SET THEIR REGULAR EXPRESSION
		if (!$remarksConfig) {
			return [];
		}

		// the processed remarks
		$output = [];

		// BREAK AND LOOP THROUGH EACH LINE OF THE REMARKS
		$remarksLines = explode("\n", $remarksBulk);
		foreach ($remarksLines as $remarkLineIndex=>$remarkLine) {
			// clean and check the current remark line
			$remarkLine = trim($remarkLine);

			if ($remarkLine) {
				// TRY TO MATCH THIS LINE WITH ANY OF THE REMARK FORMATS ------------------------------
				foreach ($remarksConfig as $rmFormat) {

					// apply the regexp
					$matches=[];
					$matchesCount = preg_match_all($rmFormat['regExp'], $remarkLine, $matches, PREG_PATTERN_ORDER);
					// check match output
					if ($matchesCount===false) {
						throw new \Exception("The regular expression for a remark is not valid: remark: '{$rmFormat['format']}', regexp: '{$rmFormat['regExp']}', text: {$remarkLine}.");
					}

					// we have a match, check the values
					if ($matchesCount) {
						// store the found value into the output result
						foreach ($rmFormat['vars'] as $rmIdx=>$thisVar) {
							// obtain the matched value
							$v = trim($matches[$rmIdx+1][0]);

							$valid = true;

							// VALIDATE THE VARIABLES
							// for select, check if the option is in the input data
							if ($valid && $thisVar['type']=='select') {
								$valid = false;
								foreach ($thisVar['options'] as $opt) {
									if ($opt==$v) {
										$valid = true;
									}
								}
							}

							// if type = date - check the date; if invalid, skip the storing
							if ($valid && $thisVar['type']=='date') {
								if (!validDate($v)) {
									$valid = false;
								}
							}

							if ($valid) {
								// post-format the variable
								if ($thisVar['type']=='boolean') {
									$v = (Bool)$v;
								}

								if($thisVar['type']=='blank') {
									$v = true;
								}

								// store the processed remark
								$output[] = [
									'key' => 			$thisVar['name'],
									'value' => 			$v,
									'var' => 			$thisVar,
									'lineIndex' => 		$remarkLineIndex,
									'originalRemark' => $rmFormat,
								];
							}
						}

						break;

					}
				} // end:foreach
			}
		}

		return $output;
	}
}

