<?php
	namespace Mocha\Oms;

	use Mocha\Core\InstanceKey;
	use Mocha\Core\InstanceReference;

	use Mocha\Core\KnownClassGuids;
	use Mocha\Core\KnownInstanceGuids;
	use Mocha\Core\KnownAttributeGuids;
	use Mocha\Core\KnownMethodBindingGuids;
	use Mocha\Core\KnownRelationshipGuids;
	use Mocha\Core\OmsContext;
	use Mocha\Core\TenantReference;

	use Phast\System;
	use Phast\UUID;

	abstract class Oms
	{
		public function __construct()
		{
			$this->_tenant = null;
		}
		
		/**
		 * Gets the instance with the specified global identifier.
		 */
		public function getInstanceByGlobalIdentifier(UUID|string $inst) : ?InstanceReference
		{
			return null;
		}

		/**
		 * Indices are 1-based
		 */
		public function getNthInstanceOf(InstanceReference|string|InstanceKey $sourceInstance, int $index, $defaultValue = null)
		{
			$insts = $this->getRelatedInstances($sourceInstance, KnownRelationshipGuids::Class__has__Instance);
			if (count($insts) > 0)
			{
				return $insts[$index - 1];
			}
			return $defaultValue;
		}

		public function getRelatedInstances(InstanceReference|string|InstanceKey $sourceInstance, InstanceReference|string|InstanceKey|null $relationshipInstance, \DateTime $effectiveDate = null) : ?array
		{
			$tenant = $this->getTenant();
			if ($tenant === null)
			{
				trigger_error("tenant cannot be null", \E_USER_ERROR);
				return null;
			}
			
			$sourceInstance = $this->normalizeInstanceReference($sourceInstance);
			$relationshipInstance = $this->normalizeInstanceReference($relationshipInstance);
			if ($relationshipInstance === null)
				return null;
			
			return $this->getRelatedInstancesInternal($sourceInstance, $relationshipInstance, $effectiveDate);
		}
		protected abstract function getRelatedInstancesInternal(InstanceReference $sourceInstance, InstanceReference $relationshipInstance, \DateTime $effectiveDate = null) : ?array;
		
		public function getRelatedInstance(InstanceReference|string|InstanceKey $sourceInstance, InstanceReference|string|InstanceKey $relationshipInstance, \DateTime $effectiveDate = null) : ?InstanceReference
		{
			$insts = $this->getRelatedInstances($sourceInstance, $relationshipInstance, $effectiveDate);
			if ($insts === null)
				return null;
			if (count($insts) === 0)
				return null;
			return $insts[0];
		}

		/**
		 * Gets the parent class Instance of the specified Instance.
		 */
		public function getParentClass(InstanceReference $inst) : ?InstanceReference
		{
			$gid = $inst->GlobalIdentifier->__toString();
			$forClass = $this->getRelatedInstance($inst, $this->getInstanceByGlobalIdentifier(KnownRelationshipGuids::Instance__for__Class));
			return $forClass;
		}

		/**
		 * Gets the instance representing the default tenant.
		 */
		public function getTenantInstance() : ?InstanceReference
		{
			return $this->getInstanceByGlobalIdentifier(KnownInstanceGuids::DefaultTenant);
		}

		/**
		 * Gets the value of the given attribute on the specified instance.
		 */
		public function getAttributeValue(InstanceReference|string|InstanceKey|null $sourceInstance, InstanceReference|string|InstanceKey|null $attributeInstance, $defaultValue = null, $effectiveDate = null) : ?string
		{
			$tenant = $this->getTenant();
			if ($tenant === null)
			{
				trigger_error("tenant cannot be null", \E_USER_ERROR);
				return null;
			}
			
			$sourceInstance = $this->normalizeInstanceReference($sourceInstance);
			$attributeInstance = $this->normalizeInstanceReference($attributeInstance);
			if ($sourceInstance === null || $attributeInstance === null)
				return $defaultValue;
			
			return $this->getAttributeValueInternal($sourceInstance, $attributeInstance, $defaultValue, $effectiveDate);
		}
		protected abstract function getAttributeValueInternal(InstanceReference $sourceInstance, InstanceReference $attributeInstance, $defaultValue = null, $effectiveDate = null) : ?string;

		public function setAttributeValue(InstanceReference|string|InstanceKey $sourceInstance, InstanceReference|string|InstanceKey $attributeInstance, mixed $value, ?\DateTime $effectiveDate = null)
		{
			$tenant = $this->getTenant();
			if ($tenant === null)
			{
				trigger_error("tenant cannot be null", \E_USER_ERROR);
				return null;
			}

			$sourceInstance = $this->normalizeInstanceReference($sourceInstance);
			$attributeInstance = $this->normalizeInstanceReference($attributeInstance);
			$this->setAttributeValueInternal($sourceInstance, $attributeInstance, $value, $effectiveDate);
		}
		protected abstract function setAttributeValueInternal(InstanceReference $sourceInstance, InstanceReference $attributeInstance, mixed $value, ?\DateTime $effectiveDate = null);

		public function getDefaultTaskUrl(InstanceReference $inst) : ?string
		{
			$instParent = $this->getParentClass($inst);
			$instDefaultTask = $this->getRelatedInstance($instParent, KnownRelationshipGuids::Class__has_default__Task);
			if ($instDefaultTask != null)
			{
				// !FIXME: check to see if the task is authorized for the current user
				// (e.g. this can be by if `User .has Security Domain` any in `Securable.has Security Domain`)
				// e.g. if User has `Administrator` and Task has `Administrator`, return true
				
				if (true) // ($oms->isTaskAuthorizedForUser($instDefaultTask, $oms->getCurrentUser()))
				{
					return System::ExpandRelativePath("~/d/inst/" . $instParent->InstanceKey . "/" . $inst->InstanceKey . ".htmld");
				}
			}
			return null;
		}
		public function getInstanceText($inst)
		{
			$parentClass = $this->getParentClass($inst);
			$instancesLabeledByRAMB = $this->getRelatedInstance($parentClass, KnownRelationshipGuids::Class__instances_labeled_by__Executable_returning_Attribute);
			if ($instancesLabeledByRAMB !== null)
			{
				$context = new OmsContext();
				$context->setWorkData($parentClass->GlobalIdentifier->__toString(), $inst);
				$textAttr = $this->executeMethod($context, $instancesLabeledByRAMB);
				$text = $context->getWorkData($textAttr);
				return $text;
			}
			return $this->getAttributeValue($inst, $this->getInstanceByGlobalIdentifier(KnownAttributeGuids::Name));
		}

		public function assignRelationship(InstanceReference|string|InstanceKey $sourceInstance, InstanceReference|string|InstanceKey $relationshipInstance, InstanceReference|string|InstanceKey|array $targetInstances, ?\DateTime $effectiveDate = null)
		{
			$tenant = $this->getTenant();
			if ($tenant === null)
			{
				trigger_error("tenant cannot be null", \E_USER_ERROR);
				return null;
			}

			$sourceInstance = $this->normalizeInstanceReference($sourceInstance);
			$relationshipInstance = $this->normalizeInstanceReference($relationshipInstance);
			$targetInstances = $this->normalizeInstanceReference($targetInstances);
			return $this->assignRelationshipInternal($sourceInstance, $relationshipInstance, $targetInstances, $effectiveDate);
		}
		protected abstract function assignRelationshipInternal(InstanceReference $sourceInstance, InstanceReference $relationshipInstance, InstanceReference|array $targetInstances, ?\DateTime $effectiveDate = null);

		private $_tenant;
		public function getTenant()
		{
			return $this->_tenant;
		}
		public function setTenant(TenantReference $tenant)
		{
			$oldTenant = $this->_tenant;
			$this->_tenant = $tenant;
			$this->onTenantChanged($oldTenant, $tenant);
		}

		public function onTenantChanged(TenantReference $oldTenant, TenantReference $newTenant)
		{
		}

		public function getTenantName() : ?string
		{
			if ($this->_tenant !== null)
			{
				return $this->_tenant->Name;
			}
			return null;
		}

		/**
		 * Gets the tenant with the specified name, or NULl if no tenant with the specified name exists on the server.
		 * 
		 * @param string $tenantName the name of the desired tenant
		 * 
		 * @return ?TenantReference
		 */
		public function getTenantByName(string $tenantName) : ?TenantReference
		{
			return null;
		}

		public function printInstanceKeys(array $instanceReferences, string $separator = ":")
		{
			$ct = count($instanceReferences);
			for($i = 0; $i < $ct; $i++)
			{
				echo($instanceReferences[$i]->InstanceKey);
				if ($i < $ct - 1)
				{
					echo ($separator);
				}
			}
		}

		public function instanceSetContains(array /*<InstanceReference>*/ $haystack, InstanceReference $needle)
		{
			foreach ($haystack as $v)
			{
				if ($v->DatabaseId == $needle->DatabaseId)
				{
					return true;
				}
			}
			return false;
		}

		public function getInstancesOf(InstanceReference $classInstance)
		{
			$instances = $this->getRelatedInstances($classInstance, KnownRelationshipGuids::Class__has__Instance);
			return $instances;
		}

		public function executeMethodReturningInstanceSet(InstanceReference $method, array $parms) : ?array
		{
			$parentClass = $this->getParentClass($method);
			if ($parentClass->GlobalIdentifier->__is_equal(UUID::parse(KnownClassGuids::ReturnInstanceSetMethodBinding)))
			{
				//return $this->executeMethodReturningInstanceSet($methodBindingHasMethod, $parms);
			}
			else if ($parentClass->GlobalIdentifier->__is_equal(UUID::parse(KnownClassGuids::GetSpecifiedInstancesMethod)))
			{
				$instances = $this->getRelatedInstances($method, KnownRelationshipGuids::Get_Specified_Instances_Method__uses__Instance);
				return $instances;
			}
			else if ($parentClass->GlobalIdentifier->__is_equal(UUID::parse(KnownClassGuids::GetInstancesMethod)))
			{
				$selectsOfClass = $this->getRelatedInstance($method, KnownRelationshipGuids::Get_Instances_Method__selects_instances_of__Class);
				return $this->getInstancesOf($selectsOfClass);
			}
			else
			{
				echo("zq: executeMethod: unknown parent class '" . $parentClass->GlobalIdentifier . "' [" . $parentClass->InstanceKey . "]");
				die();
			}

			return null;
		}

		/**
		 * Given an InstanceReference, GUID (string), or InstanceKey, return the normalized InstanceReference for that representation.
		 * 
		 * @return InstanceReference|array|null
		 */
		public function normalizeInstanceReference(InstanceReference|string|InstanceKey|array|null $instanceReferenceOrGuidOrInstanceKey) : InstanceReference|array|null
		{
			if ($instanceReferenceOrGuidOrInstanceKey === null)
				return null;

			if (is_array($instanceReferenceOrGuidOrInstanceKey))
			{
				$arry = array();
				foreach ($instanceReferenceOrGuidOrInstanceKey as $instanceRef)
				{
					$arry[] = $this->normalizeInstanceReference($instanceRef);
				}
				return $arry;
			}
			else if (is_string($instanceReferenceOrGuidOrInstanceKey) || $instanceReferenceOrGuidOrInstanceKey instanceof UUID)
			{
				// assume GUID
				return $this->getInstanceByGlobalIdentifier($instanceReferenceOrGuidOrInstanceKey);
			}
			else if ($instanceReferenceOrGuidOrInstanceKey instanceof InstanceKey)
			{
				return $this->getInstanceByKey($instanceReferenceOrGuidOrInstanceKey);
			}
			else if ($instanceReferenceOrGuidOrInstanceKey instanceof InstanceReference)
			{
				return $instanceReferenceOrGuidOrInstanceKey;
			}
			echo ("ERR");
			return null;
		}

		public function getTranslationValue($sourceInstance, $relationshipInstance, $defaultValue = null)
		{
			$sourceInstance = $this->normalizeInstanceReference($sourceInstance);
			$relationshipInstance = $this->normalizeInstanceReference($relationshipInstance);

			$ctx = new OmsContext();
			$m_Translatable__get__Translation_Value_for_Relationship_parm = $this->getInstanceByGlobalIdentifier(KnownMethodGuids::Translatable__get__Translation_Value_for_Relationship_parm);

			$ctx->setWorkData(KnownClassGuids::Relationship, $relationshipInstance);

			$mb_Translatable__get__Translation_Value_for_Relationship_parm = $this->executeMethodReturningAttribute($m_Translatable__get__Translation_Value_for_Relationship_parm, $ctx);
			// Translatable@get Translation Value for Relationship parm(GRA)*S(public)[ramb]
			// loop on instance seTranslation Value for Relationship parm(GRA)*S(public)[ramb]
			// loop on instance settTranslation Value for Relationship parm(GRA)*S(public)[ramb]
			// loop on instance set: Translatable@get Translations for Effective User Language(GRS)*S[rsmb]
			// ---- Translatable@get Translations for Language parm(GRS)*S[rsmb]
			// ---- Language => User@get Effective Language(SSC)[rsmb]
			// ---- ---- if User@get Current Language	then	User@get Current Language
			// ---- ---- if not User@get Current Language	then	Tenant@get Default Language
			
			// get attribute: Translation@get Value(GA)[ramb]

		}

		public function getRelatedTasks(InstanceReference $inst)
		{
			$instParent = $this->getParentClass($inst);
			
			$relatedTasks = [];
			
			$instInstance = $this->getInstanceByGlobalIdentifier(KnownClassGuids::Instance);
			$relatedTasks0 = $this->getRelatedInstances($instInstance, KnownRelationshipGuids::Class__has_related__Task);
			foreach ($relatedTasks0 as $task)
			{
				$relatedTasks[] = $task;
			}

			$relatedTasks1 = $this->getRelatedInstances($instParent, KnownRelationshipGuids::Class__has_related__Task);
			foreach ($relatedTasks1 as $task)
			{
				$relatedTasks[] = $task;
			}

			$superclasses = $this->getRelatedInstances($instParent, KnownRelationshipGuids::Class__has_super__Class);
			foreach ($superclasses as $superclass)
			{
				$relatedTasks2 = $this->getRelatedInstances($superclass, KnownRelationshipGuids::Class__has_related__Task);
				foreach ($relatedTasks2 as $task)
				{
					$relatedTasks[] = $task;
				}
			}
			return $relatedTasks;
		}

		public function getNthRelatedInstance(InstanceReference $inst, InstanceReference|string|InstanceKey|array $rel, int $index)
		{
			$rel = $this->normalizeInstanceReference($rel);
			$relInsts = $this->getRelatedInstances($inst, $rel);
			return $relInsts[$index];
		}

		/**
		 * Checks to ensure the given value is of the appropriate type for the specified work data.
		 * 
		 * @return bool true if the given value is of the appropriate type; false otherwise.
		 */
		public function validateWorkDataType(InstanceReference $instWorkData, mixed $value, &$expectedTypeName, &$actualTypeName)
		{
			$actualTypeName = gettype($value);
			if (is_string($value))
			{
				$expectedTypeName = "Text Attribute";
				if ($this->is_a($instWorkData, KnownClassGuids::TextAttribute))
				{
					return true;
				}
			}
			else if (is_int($value) || is_float($value))
			{
				$expectedTypeName = "Numeric Attribute";
				// int and float should only be assigned to Numeric Attribute
				if ($this->is_a($instWorkData, KnownClassGuids::NumericAttribute))
				{
					return true;
				}
			}
			else if ($value instanceof InstanceReference)
			{
				// instance references can be assigned to Work Sets and all sorts of other stuff
				if ($this->is_a($instWorkData, KnownClassGuids::WorkSet))
				{
					$expectedTypeName = "Work Set";
					// but if our Work Set declares a `Work Set.has valid Class` then we should validate that
					$workDataHasValidClasses = $this->getRelatedInstances($instWorkData, KnownRelationshipGuids::Work_Set__has_valid__Class);
					if ($workDataHasValidClasses !== null && count($workDataHasValidClasses) > 0)
					{
						// loop through all valid classes and check them
						foreach ($workDataHasValidClasses as $validClass)
						{
							if ($this->is_a($instWorkData, $validClass))
							{
								$expectedTypeName = $this->getAttributeValue($validClass, KnownAttributeGuids::Name);
								// our work set declares a valid class, and it passes
								return true;
							}
						}
						// none of the valid classes pass validation for $instWorkData
						$actualTypeName = $this->getAttributeValue($this->getParentClass($instWorkData), KnownAttributeGuids::Name);
						$expectedTypeName = $this->getAttributeValue($validClass, KnownAttributeGuids::Name);
						return false;
					}

					// this work set doesn't care whether or not $instWorkData passes validation
					return true;
				}
				
				// we are not a work set, so we can't validate anything
				return true;
			}
			
			// none of our validations pass
			return false;
		}

		public function executeMethod(OmsContext $context, InstanceReference $methodInstance)
		{
			$methodInstanceClass = $this->getParentClass($methodInstance);
			//$context->setWorkData("{d53d7283-92a2-4a62-b8f2-cf0a0b975634}", "blah blah");

			if ($methodInstanceClass->GlobalIdentifier->__is_equal(UUID::parse(KnownClassGuids::ReturnAttributeMethodBinding)))
			{
				$methodBindingExecutesMethod = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Method_Binding__executes__Method);
				if ($methodBindingExecutesMethod === null)
				{
					echo ("RAMB executes method not found for " . $methodInstance->InstanceKey);die();
				}
				//echo ("RAMB stack: " . $methodInstance->InstanceKey . ":"  . $methodBindingExecutesMethod->InstanceKey);
				return $this->executeMethod($context, $methodBindingExecutesMethod);
			}
			else if ($methodInstanceClass->GlobalIdentifier->__is_equal(UUID::parse(KnownClassGuids::GetAttributeMethod)))
			{
				$forClass = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Method__for__Class);
				$returnsAttribute = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Get_Attribute_Method__has__Attribute);
				
				$targetInstance = $context->workData[$forClass->GlobalIdentifier->__toString()];
				$value = $this->getAttributeValue($targetInstance, $returnsAttribute);
				$context->workData[$returnsAttribute->GlobalIdentifier->__toString()] = $value;
				return $returnsAttribute;
			}
			else if ($methodInstanceClass->GlobalIdentifier->__is_equal(UUID::parse(KnownClassGuids::GetReferencedAttributeMethod)))
			{
				$forClass = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Method__for__Class);
				$returnsAttribute = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Get_Referenced_Attribute_Method__returns__Attribute);
				$loopOnInstanceSet = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Get_Referenced_Attribute_Method__uses_reference__Executable_returning_Instance_Set);
				$answer = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Get_Referenced_Attribute_Method__uses_answer__Executable_returning_Attribute);
				
				$targetInstance = $context->workData[$forClass->GlobalIdentifier->__toString()];

				$refISRel = $this->executeMethod($context, $loopOnInstanceSet);
				$refIS = $context->getWorkData($refISRel);
				//$refIS = $this->getRelatedInstance($targetInstance, $loopOnInstanceSet);
				$value = $this->getAttributeValue($refIS, $answer);
				$context->setWorkData($returnsAttribute, $value);
				return $returnsAttribute;
			}
			else if ($methodInstanceClass->GlobalIdentifier->__is_equal(UUID::parse(KnownClassGuids::GetReferencedInstanceSetMethod)))
			{
				$forClass = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Method__for__Class);
				$returnsWorkSet = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Get_Referenced_Instance_Set_Method__returns__Work_Set);
				$loopOnInstanceSet = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Get_Referenced_Instance_Set_Method__uses_reference__Executable_returning_Instance_Set);
				$relationship = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Get_Referenced_Instance_Set_Method__uses_answer__Executable_returning_Instance_Set);
				
				$targetInstance = $context->workData[$forClass->GlobalIdentifier->__toString()];
				$value = $this->getRelatedInstance($targetInstance, $relationship);
				$context->setWorkData($returnsWorkSet, $value);
				return $returnsWorkSet;
			}
			else if ($methodInstanceClass->GlobalIdentifier->__is_equal(UUID::parse(KnownClassGuids::BuildAttributeMethod)))
			{
				$returnsAttribute = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Build_Attribute_Method__returns__Attribute);
				$initialValue = $this->getAttributeValue($methodInstance, KnownAttributeGuids::Value);
				$buildsWithAttributes = $this->getRelatedInstances($methodInstance, KnownRelationshipGuids::Build_Attribute_Method__builds_with__Executable_returning_Attribute);
				
				$value = $initialValue;
				foreach ($buildsWithAttributes as $buildsWithAttribute)
				{
					$attr = $this->executeMethod($context, $buildsWithAttribute);
					if ($attr !== null)
					{
						$val = $context->getWorkData($attr);
						if (is_string($val))
						{
							$value .= $val;
						}
						else
						{
							$value .= $val->InstanceKey;
						}
					}
					else
					{
						trigger_error("executeMethod did not return properly - called method: " . $buildsWithAttribute->GlobalIdentifier . " [" . $buildsWithAttribute->InstanceKey . "]");
					}
				}
				$context->setWorkData($returnsAttribute, $value);
				return $returnsAttribute;
			}
			else if ($methodInstanceClass->GlobalIdentifier->__is_equal(UUID::parse(KnownClassGuids::ProcessRelatedUpdatesMethod)))
			{
				$classForPRU = $this->getRelatedInstance($methodInstance, KnownRelationshipGuids::Process_Related_Updates_Method__processes_for__Class);
				if ($classForPRU !== null)
				{
				}

				//!HACK!
				$instForPRU = $this->getNthInstanceOf($classForPRU, 1);
				if ($instForPRU !== null)
				{
					//echo("current work data for '" . $instForPRU->GlobalIdentifier . "' = '" . $context->getWorkData($instForPRU->GlobalIdentifier->__toString()) . "'");
				}

				$instExecutablesForPUMB = $this->getRelatedInstances($methodInstance, KnownRelationshipGuids::Process_Related_Updates_Method__uses__Executable_for_PUMB);
				foreach ($instExecutablesForPUMB as $instExecutableForPUMB)
				{
					if ($instExecutableForPUMB !== null)
					{
						$assignsAttribute = $this->getRelatedInstance($instExecutableForPUMB, KnownRelationshipGuids::Assign_Attribute_Method__assigns__Attribute);
						$usesExecutableReturningAttribute = $this->getRelatedInstance($instExecutableForPUMB, KnownRelationshipGuids::Assign_Attribute_Method__uses__Executable_returning_Attribute);

						//$context->setWorkData($assignsAttribute->GlobalIdentifier->__toString(), $usesExecutableReturningAttribute);
						//echo ("assigning attribute '" . $assignsAttribute->GlobalIdentifier . "' the value from executable '" . $usesExecutableReturningAttribute->GlobalIdentifier . "'");

						$value = $context->getWorkData($usesExecutableReturningAttribute->GlobalIdentifier->__toString());
						if ($value instanceof InstanceReference)
						{
							//echo ("the value from work data is ##IS#'" . $value->GlobalIdentifier . "'##");
						}
						else
						{
							$this->setAttributeValue($instForPRU, $assignsAttribute, $value);
						}
					}
					else
					{
						echo ("executable for PUMB is null");
					}
				}
			}
			else
			{
				trigger_error("unknown method class '" . $methodInstanceClass->GlobalIdentifier . "' [" . $methodInstanceClass->InstanceKey . "]");
			}
		}

		public function is_a(InstanceReference $inst, InstanceReference|string|InstanceKey $what)
		{
			$pclass = $this->getParentClass($inst);
			if ($pclass === null)
				return false;
			if ($pclass->__is_equal($this->normalizeInstanceReference($what)))
				return true;
			return false;
		}
	}
?>
