<?php

namespace App\ImportedOrder\Convertor;

/**
 * Converts an imported order stdClass object to imported order object.
 * @author Gabriel
 */
class ToImportedOrderObject
{

	/**
	 * The input - a (JSON) stdClass object.
	 * @var \stdClass
	 */
	private \stdClass $importedOrderStd;

	/**
	 * Defines the Imported Order structure.
	 */
	use ImportedOrderStructureTrait;

	/**
	 * The internal cache for all imported order entities that are reused throughout the IO object.
	 * @var array
	 */
	private array $cachedImportedOrderEntities = [];


	public function __construct(\stdClass $importedOrderStd)
	{
		$this->importedOrderStd = $importedOrderStd;
	}


	/**
	 * Processes the imported order stdClass object and returns the imported order object.
	 * The services are treated in a more complex way; first when calling prepareReusedObjectRefs()
	 * we only store the services object refs. and then at the very end we also call the recursive
	 * method loadObjectAttributes() which loads all data "in depth". This is done like this because
	 * we have some service fields which point to references to another service on the order (eg.
	 * linkedToService, packageParentService) and given that we cannot guarantee the order of the
	 * services being referenced, we first have to load them with minimum data (based on their index)
	 * and then we can go recursively and load everything else.
	 * @return \App\ImportedOrder\Order
	 */
	public function getImportedOrderObject() :\App\ImportedOrder\Order
	{

		$ioObj = new \App\ImportedOrder\Order();

		// loads all common objects refs first (passengers, corporations, etc, but for services it does not load the data recursively but only the object ref)
		$this->prepareReusedObjectRefs($ioObj);

		// loads all order data recursively (without
		$this->loadObjectAttributes($ioObj, $this->importedOrderStd);

		// load all the services data recursively
		if (property_exists($this->importedOrderStd, 'services') and $this->importedOrderStd->services) {
			foreach ($this->importedOrderStd->services as $idx=>$serviceStd) {
				$this->loadObjectAttributes($this->cachedImportedOrderEntities['Service'][$idx], $serviceStd);
			}
		}

		return $ioObj;

	}

	/**
	 * Injects the main entities into the IO object and also caches internally these entities
	 * based on their index in the list.
	 * @param \App\ImportedOrder\Order $ioObj
	 */
	private function prepareReusedObjectRefs(\App\ImportedOrder\Order $ioObj) :void
	{

		$this->cachedImportedOrderEntities['Supplier'] = [];
		if (property_exists($this->importedOrderStd, 'suppliers') and $this->importedOrderStd->suppliers) {
			$ioObj->suppliers = [];
			foreach ($this->importedOrderStd->suppliers as $idx=>$supplierStd) {
				$supplierObj = new \App\ImportedOrder\Supplier();
				$this->loadObjectAttributes($supplierObj, $supplierStd);
				$this->cachedImportedOrderEntities['Supplier'][$idx] = $supplierObj;
				$ioObj->suppliers[$idx] = $supplierObj;
			}
		}

		$this->cachedImportedOrderEntities['Corporate'] = [];
		if (property_exists($this->importedOrderStd, 'corporates') and $this->importedOrderStd->corporates) {
			$ioObj->corporates = [];
			foreach ($this->importedOrderStd->corporates as $idx=>$corporateStd) {
				$corporateObj = new \App\ImportedOrder\Corporate();
				$this->loadObjectAttributes($corporateObj, $corporateStd);
				$this->cachedImportedOrderEntities['Corporate'][$idx] = $corporateObj;
				$ioObj->corporates[$idx] = $corporateObj;
			}
		}

		$this->cachedImportedOrderEntities['Passenger'] = [];
		if (property_exists($this->importedOrderStd, 'passengers') and $this->importedOrderStd->passengers) {
			$ioObj->passengers = [];
			foreach ($this->importedOrderStd->passengers as $idx=>$passengerStd) {
				$passengerObj = new \App\ImportedOrder\Passenger();
				$this->loadObjectAttributes($passengerObj, $passengerStd);
				$this->cachedImportedOrderEntities['Passenger'][$idx] = $passengerObj;
				$ioObj->passengers[$idx] = $passengerObj;
			}
		}

		$this->cachedImportedOrderEntities['SupplierProduct'] = [];
		if (property_exists($this->importedOrderStd, 'supplierProducts') and $this->importedOrderStd->supplierProducts) {
			$ioObj->supplierProducts = [];
			foreach ($this->importedOrderStd->supplierProducts as $idx=>$suppProdStd) {
				$suppProdObj = new \App\ImportedOrder\SupplierProduct();
				$this->loadObjectAttributes($suppProdObj, $suppProdStd);
				$this->cachedImportedOrderEntities['SupplierProduct'][$idx] = $suppProdObj;
				$ioObj->supplierProducts[$idx] = $suppProdObj;
			}
		}

		$this->cachedImportedOrderEntities['Service'] = [];
		if (property_exists($this->importedOrderStd, 'services') and  $this->importedOrderStd->services) {
			$ioObj->services = [];
			foreach ($this->importedOrderStd->services as $idx=>$serviceStd) {
				$serviceObj = new \App\ImportedOrder\Service();
//				$this->loadObjectAttributes($serviceObj, $serviceStd, false); // this is commented as we only want to associate the index to the empty object; we'll do the rest later - check comments on getImportedOrderObject() call
				$this->cachedImportedOrderEntities['Service'][$idx] = $serviceObj;
				$ioObj->services[$idx]  = $serviceObj;
			}
		}

	}


	/**
	 * Main call for all entities - is recursively called to go in depth on all objects.
	 * @param \App\ImportedOrder\EntityAbstract $ioEntityObj
	 * @param \stdClass $ioEntityStd
	 */
	private function loadObjectAttributes(\App\ImportedOrder\EntityAbstract $ioEntityObj, \stdClass $ioEntityStd) :void
	{

		$className = stripClassNamespace(get_class($ioEntityObj));

		$entityConfigs = $this->importedOrderStructure[$className]??[];

		foreach ($ioEntityStd as $field=>$value) {

			$entityFieldConfigs = $entityConfigs[$field]??[];

			// SCALAR ATTRIBUTE VALUES (OR INVALID/OBSOLETE FIELD) ////////////////////////////////
			if (!$entityFieldConfigs) {
				// consider the attribute only if the property exists (this way old/deprecated/obsolete/eliminated properties are skipped)
				if (property_exists($ioEntityObj, $field)) {
					if (is_scalar($value)) {
						$ioEntityObj->$field = $value;
					} elseif ($value!==null) {
						throw new Exception("Invalid unserialization for for class `{$className}`, field `{$field}` - the value should have been a scalar value or there is a problem with the imported order structure definition.");
					}
				}
			}
			// ARRAY ATTRIBUTES ///////////////////////////////////////////////////////////////////
			elseif ($entityFieldConfigs['type']=='array') {
				if (is_array($value) or is_object($value)) {
					$ioEntityObj->$field = json_decode(json_encode($value), true); // deep conversion to array (is needed for extraInfo or taxCodes)
				} elseif ($value!==null) {
					throw new Exception("Invalid unserialization for for class `{$className}`, field `{$field}` - the value should have been an array value or there is a problem with the imported order structure definition.");
				}
			} elseif ($entityFieldConfigs['type']=='array|scalar') {
				if (is_scalar($value) or is_array($value)) {
					$ioEntityObj->$field = $value;
				} elseif (is_object($value)) {
					$ioEntityObj->$field = json_decode(json_encode($value), true);
				} elseif ($value!==null) {
					throw new Exception("Invalid unserialization for for class `{$className}`, field `{$field}` - the value should have been an array or a scalar value or there is a problem with the imported order structure definition.");
				}
			}
			// DATETIME ATTRIBUTES ////////////////////////////////////////////////////////////////
			elseif  ($entityFieldConfigs['type']=='datetime') {
				if ($value!==null) {
					$ioEntityObj->$field = \Engine\DateTime::create($value);
				}
			}
			// TICKET NUMBERS /////////////////////////////////////////////////////////////////////
			elseif  ($entityFieldConfigs['type']=='tickets') {
				if ($value) {
					foreach ($value as $ticket){
						$psgObj = $this->cachedImportedOrderEntities['Passenger'][$ticket->passenger];
						if (!$psgObj) {
							throw new Exception( sprintf("Could not get the passenger object from its index when collecting ticket numbers onto the offer; passenger index `%s`; offer id = `%s`.", $ticket->passenger, $ioEntityObj->idSystem) );
						}
						$ioEntityObj->addTicket($psgObj, $ticket->number, property_exists($ticket, 'conjunctions') ? (array)$ticket->conjunctions : []);
					}
				}
			}
			// SPECIFIC OBJECT RELATED STUFF (object, objectRef, objectsList, objectsListRef) /////
			else {

				$ioEntityClassName = $entityFieldConfigs['unserializeToClassName'];
				if (!$ioEntityClassName) {

					throw new \Exception("Missing key `unserializeToClassName` in the imported order structure definition for class `{$className}` field `{$field}`.");
				}
				$ioEntityClassNameFull = "\App\\ImportedOrder\\{$ioEntityClassName}";

				if (!class_exists($ioEntityClassNameFull)) {
					throw new Exception("Invalid class name `{$ioEntityClassName}` for parent class `{$className}` in imported order namespace - check imported order structure.");
				}


				switch ($entityFieldConfigs['type']) {
					// OBJECT ---------------------------------------------------------------------
					case 'object': {

						$childIoEntityObj = new $ioEntityClassNameFull();

						// populate the imported order entity object with its fields and values
						$this->loadObjectAttributes($childIoEntityObj, $value);

						// associate this entity IO child object to the parent
						$ioEntityObj->$field = $childIoEntityObj;

						break;
					}

					// OBJECT REF -----------------------------------------------------------------
					case 'objectRef': {
						if ($value!==null) {

							// special treatment for default passport for passengers
							if ($field=='defaultPassport') {
								$ioEntityObj->defaultPassport = $ioEntityObj->passports[$value];
							}
							// special treatment for service chosen offer
							elseif ($field=='chosenOffer') {
								$ioEntityObj->chosenOffer = $ioEntityObj->offers[$value];
							}
							// get the object from the main entities kept as references
							else {
								$childIoEntityObj = $this->cachedImportedOrderEntities[$ioEntityClassName][$value];
								if (!$childIoEntityObj) {
									throw new Exception("Missing reference for imported order entity type `{$ioEntityClassName}` used on `{$field}` attribute for `".get_class($ioEntityObj)."` on index `{$value}`.");
								}
								$ioEntityObj->$field = $childIoEntityObj;
							}
						}
						break;
					}

					// OBJECTS LIST ---------------------------------------------------------------
					case 'objectsList': {

						if (in_array($field, ['corporates', 'passengers', 'suppliers', 'supplierProducts', 'services'])) {
							break;
						}

						if ($value) {
							$ioEntityObj->$field = [];
							foreach ($value as $record) {

								$childIoEntityObj = new $ioEntityClassNameFull();

								// populate the imported order entity object with its fields and values
								$this->loadObjectAttributes($childIoEntityObj, $record);

								// associate this entity IO child object to the parent
								$ioEntityObj->$field[] = $childIoEntityObj;

							}
						}
						break;
					}

					// OBJECTS LIST REF -----------------------------------------------------------
					case 'objectsListRef': {

						if ($value) {
							$ioEntityObj->$field = [];
							foreach ($value as $item) {

								// get IO entity from cache based on index
								$childIoEntityObj = $this->cachedImportedOrderEntities[$ioEntityClassName][$item];

								if (!is_object($childIoEntityObj)) {
									throw new Exception("Missing reference for imported order entity type `{$ioEntityClassName}` used on `{$field}` attribute for `".get_class($ioEntityObj)."` on index `{$item}`.");
								}

								// associate this entity IO child object to the parent
								$ioEntityObj->$field[] = $childIoEntityObj;
							}
						}
						break ;
					}

					default: {
						throw new \Exception("Invalid type `{$entityFieldConfigs['type']}` for class `{$className}` field `{$field}` when converting to importedOrder object.");
					}

				}

			}


		}

	}

}
