<?php
namespace ModulesAbstract\PluginReports\ExtraSupport\UDEF\Model;

/**
 * Process the UDEF config structure
 * To append a list of available fields, overwrite the constructor and append them to $this->availableFields
 *
 *
 * @author Lucian Sirbu
 */
class DataProcessorAutomatic extends DataProcessor
{
	/**
	 * Available fields
	 * @var array:string
	 */
	protected $availableFields = null;

	/**
	 * List of entities or various fields, that are used by getField()
	 * @var array
	 */
	protected $sources= [];


	/**
	 * Initialize the processor class
	 */
	public function __construct()
	{
		// nothing here yet
	}

	/**
	 * Get the available list of sources
	 * @return array
	 */
	public function getFields()
	{
		if ($this->availableFields === null) {
			$this->availableFields = $this->internalGetAvailableFields();
		}

		return $this->availableFields;
	}

	/**
	 * Get remarks from a specific category
	 *
	 * @param string $category
	 * @return array
	 */
	public function getFieldsFromRemarkCategory(string $category)
	{
		// the raw list of remarks. We will extract variables from this list
		$remarks = \App\Config\Remarks::getInstance()->getAllRemarksFromCategory($category);

		// extract & deduplicate the variables list of each remark
		$fields = [];
		foreach ($remarks as $remark) {
			foreach($remark['vars'] as $varK => $var) {
				$internalKey = sprintf('%s.%s', $category, $var['name']);
				// we need to be able to sort by remark name & also de-duplicate the remark names
				$fields[$internalKey] = [
					'key' => 		sprintf("%s.processedRemarks.%s", $category, $var['name']),
					'label' => 		sprintf("%s.processedRemarks.%s", $category, $var['name']),
					'sortField' => 	[
						$remark['format'],
						$varK,
						$internalKey
					],
				];
			}
		}

		// sort by sortField
		usort($fields, function($a, $b) {
			$ret = 0;

			if (!$ret) {
				$ret = strcasecmp($a['sortField'][0], $b['sortField'][0]);
			}

			if (!$ret) {
				$ret = strcasecmp($a['sortField'][1], $b['sortField'][1]);
			}

			if (!$ret) {
				$ret = strcasecmp($a['sortField'][2], $b['sortField'][2]);
			}

			return $ret;
		});

		// remove the sortField
		$fields = array_values(array_map(function($field) {
			unset($field['sortField']);
			return $field;
		}, $fields));

		return $fields;
	}


	/**
	 * Get the available list of sources
	 * NOTE: You should override this method and append the list of available fields
	 *
	 * @return array
	 */
	protected function internalGetAvailableFields()
	{
		$availableFields =  [
			'blank',
		];

		// NOTE: this is how you should inject the list of available remarks for a certain category
		// $availableFields = array_merge($availableFields,
		// 	array_map(function ($f) {
		// 		return $f['key'];
		// 	}, $this->getFieldsFromRemarkCategory('service'))
		// );

		return $availableFields;
	}

	/**
	 * Analyze the input field (Ex: invoice.id) and return the list of fields
	 * @param string $field
	 * @return array
	 */
	protected function analyzeField($field)
	{
		$ret = [
			'entity' => null,
			'accessPath' => [],
		];
		$fieldComponents = explode('.', $field);

		$state = 'entity';

		$loops = 10;
		while ($state!='final' && $loops--) {
			// main entity selection
			if ($state=='entity' && in_array($fieldComponents[0], ['invoice', 'service', 'customer', 'corporate', 'passenger', ]) ) {
				$ret['entity'] = $fieldComponents[0];
				array_shift($fieldComponents);
				$state = 'attribute';
			}

			// we're accessing a sub-entity in the main entity (Ex: $invoice->customer->addUser->name)
			if ($state=='attribute' && in_array($fieldComponents[0], ['invoice', 'service', 'customer', 'corporate', 'passenger', ]) ) {
				$ret['accessPath'][] = [
					'type' => 'attribute',
					'name' => $fieldComponents[0],
				];
				array_shift($fieldComponents);
				$state = 'attribute';
			}

			// object attribute access
			if ($state=='attribute' && !in_array($fieldComponents[0], ['invoice', 'service', 'customer', 'corporate', 'passenger', 'processedRemarks', ]) ) {
				$ret['accessPath'][] = [
					'type' => 'attribute',
					'name' => $fieldComponents[0],
				];
				array_shift($fieldComponents);
				$state = 'attribute';
			}

			// array key access
			if ($state=='attribute' && in_array($fieldComponents[0], ['processedRemarks', ]) ) {
				$ret['accessPath'][] = [
					'type' => 'attribute',
					'name' => $fieldComponents[0],
				];
				array_shift($fieldComponents);

				$ret['accessPath'][] = [
					'type' => 'array',
					'name' => $fieldComponents[0],
				];
				array_shift($fieldComponents);

				$state = 'attribute';
			}

			// are we done?
			if (!$fieldComponents) {
				$state = 'final';
			}
		}

		// at this point, the list of components should be empty, and loops should be >0
		if (!$loops) {
			throw new \Exception('Invalid field: '.$field);
		}

		return $ret;
	}

	/**
	 * Process the specified field. The field value should be the exact formula (ex:'invoice.corporate.company')
	 * @param string $field
	 * @throws \RuntimeException
	 * @return string
	 */
	public function processField($field)
	{
		if (!$this->isFieldSourceIsValid($field)) {
			throw new \RuntimeException(sprintf("[%s] field '%s' is not a valid field. Should be defined in %s", __METHOD__, $field, get_called_class()));
		}

		$return = null;

		if ($field === 'blank') {
			$return = '';
		}
		else {
			$fieldAnalysis = $this->analyzeField($field);
			switch ($fieldAnalysis['entity']) {
				case 'invoice':
					if (!$this->sources['invoice']) {
						throw new \Exception(sprintf("[%s] field '%s' tried to access %s entity, but it wasn't assigned", __METHOD__, $field, $fieldAnalysis['entity']));
					}
					$return = $this->sources['invoice'];
				break;

				case 'service':
					if (!$this->sources['service']) {
						throw new \Exception(sprintf("[%s] field '%s' tried to access %s entity, but it wasn't assigned", __METHOD__, $field, $fieldAnalysis['entity']));
					}
					$return = $this->sources['service'];
				break;

				case 'passenger':
					if (!$this->sources['passenger']) {
						throw new \Exception(sprintf("[%s] field '%s' tried to access %s entity, but it wasn't assigned", __METHOD__, $field, $fieldAnalysis['entity']));
					}
					$return = $this->sources['passenger'];
				break;

				case 'corporate':
					if (!$this->sources['corporate']) {
						throw new \Exception(sprintf("[%s] field '%s' tried to access %s entity, but it wasn't assigned", __METHOD__, $field, $fieldAnalysis['entity']));
					}
					$return = $this->sources['invoice']->corporate;
				break;

				// pseudo-entity, it returns the invoice customer or the service pax, depending on the context
				case 'customer':
					$return = ($this->sources['invoice']->idCorporate?$this->sources['passenger']:$this->sources['invoice']->customer);

					if (!$return || !$return->recordExists()) {
						throw new \Exception(sprintf("[%s] field '%s' tried to access %s entity, but it wasn't assigned", __METHOD__, $field, $fieldAnalysis['entity']));
					}

				break;

				default:
					throw new \RuntimeException(sprintf("[%s] field '%s' is not a valid field [1]", __METHOD__, $field));
			}

			// now we have the main entity, we can access the sub-entities
			foreach ($fieldAnalysis['accessPath'] as $path) {
				if ($path['type']=='attribute') {
					if ($path['name']=='id') {
						$return = $return->getId();
					}
					else {
						$return = $return->{$path['name']};
					}
				}
				elseif ($path['type']=='array') {
					$return = $return[$path['name']];
				}
				else {
					throw new \RuntimeException(sprintf("[%s] field '%s' is not a valid field [2]", __METHOD__, $field));
				}
			}
		}

		return $return;
	}
}
