<?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 Mocha\Oop\MethodImplementation;
	use Mocha\Oop\Methods\BuildAttributeMethod;
	use Phast\System;
	use Phast\Utilities\Stopwatch;
	use Phast\UUID;

	abstract class Oms
	{
		public ?array $MethodImplementations;

		public function __construct()
		{
			$this->_tenant = null;
			$this->_pclass_cache = null;
			$this->MetadataExceptions = array();
			$this->MethodImplementations = null;
		}

		public abstract function getUserByUserName(string $username) : ?InstanceReference;
		
		private function registerMethodImplementation(InstanceReference|string|InstanceKey $methodImplementationClass, MethodImplementation $implementation)
		{
			$methodImplementationClass = $this->normalizeInstanceReference($methodImplementationClass);
			$this->MethodImplementations[$methodImplementationClass->GlobalIdentifier->__toString()] = $implementation;
		}

		private function initializeMethodImplementations()
		{
			$this->registerMethodImplementation(KnownClassGuids::BuildAttributeMethod, new \Mocha\Oop\MethodImplementations\BuildAttributeMethodImplementation());
			$this->registerMethodImplementation(KnownClassGuids::ConditionalSelectAttributeMethod, new \Mocha\Oop\MethodImplementations\ConditionalSelectAttributeMethodImplementation());
			$this->registerMethodImplementation(KnownClassGuids::ConditionalSelectFromInstanceSetMethod, new \Mocha\Oop\MethodImplementations\ConditionalSelectInstanceSetMethodImplementation());
			$this->registerMethodImplementation(KnownClassGuids::EvaluateBooleanExpressionMethod, new \Mocha\Oop\MethodImplementations\EvaluateBooleanExpressionMethodImplementation());
			$this->registerMethodImplementation(KnownClassGuids::GetAttributeBySystemRoutineMethod, new \Mocha\Oop\MethodImplementations\GetAttributeBySystemRoutineMethodImplementation());
			$this->registerMethodImplementation(KnownClassGuids::GetAttributeMethod, new \Mocha\Oop\MethodImplementations\GetAttributeMethodImplementation());
			$this->registerMethodImplementation(KnownClassGuids::GetInstanceSetBySystemRoutineMethod, new \Mocha\Oop\MethodImplementations\GetInstanceSetBySystemRoutineMethodImplementation());
			$this->registerMethodImplementation(KnownClassGuids::GetInstancesMethod, new \Mocha\Oop\MethodImplementations\GetInstancesMethodImplementation());
			$this->registerMethodImplementation(KnownClassGuids::GetReferencedAttributeMethod, new \Mocha\Oop\MethodImplementations\GetReferencedAttributeMethodImplementation());
			$this->registerMethodImplementation(KnownClassGuids::GetReferencedInstanceSetMethod, new \Mocha\Oop\MethodImplementations\GetReferencedInstanceSetMethodImplementation());
			$this->registerMethodImplementation(KnownClassGuids::GetSpecifiedInstancesMethod, new \Mocha\Oop\MethodImplementations\GetSpecifiedInstancesMethodImplementation());
			$this->registerMethodImplementation(KnownClassGuids::ProcessRelatedUpdatesMethod, new \Mocha\Oop\MethodImplementations\ProcessRelatedUpdatesMethodImplementation());
			$this->registerMethodImplementation(KnownClassGuids::SelectFromInstanceSetMethod, new \Mocha\Oop\MethodImplementations\SelectFromInstanceSetMethodImplementation());
		}

		private function getMethodImplementation(InstanceReference|string|InstanceKey $methodImplementationClass)
		{
			if ($this->MethodImplementations === null)
			{
				// initialize method implementations
				// we cannot do this in the constructor, since it relies on getInstanceByGlobalIdentifier
				// which fails if the current tenant is null, and we need OMS to get the current tenant...
				$this->initializeMethodImplementations();
			}

			$methodImplementationClass = $this->normalizeInstanceReference($methodImplementationClass);
			$key = $methodImplementationClass->GlobalIdentifier->__toString();
			if (array_key_exists($key, $this->MethodImplementations))
			{
				return $this->MethodImplementations[$key];
			}
			return null;
		}

		/**
		 * @var array 
		 */
		public $MetadataExceptions;
		
		/**
		 * Gets ALL instances on this OMS.
		 */
		public function getInstances() : array
		{
			return [ ];
		}
		
		/**
		 * 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 instanceKeysToInstances(array|string|null $instanceKeys)
		{
			$insts = [ ];
			if ($instanceKeys === null)
			{
				return $insts;
			}
			if (is_string($instanceKeys))
			{
				$instanceKeys = explode(",", $instanceKeys);
			}
			foreach ($instanceKeys as $instanceKey)
			{
				if (is_string($instanceKey))
				{
					$instanceKey = InstanceKey::parse($instanceKey);
				}
				if ($instanceKey !== null)
				{
					$insts[] = $this->getInstanceByKey($instanceKey);
				}
			}
			return $insts;
		}

		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)
			{
				//echo ("Relationship Instance is null");
				return null;
			}
			
			// trigger_error("MADI debug: entering " . get_class($this) . "::getRelatedInstancesInternal");
			// $stopwatch = new Stopwatch();
			// $stopwatch->start();

			$insts = $this->getRelatedInstancesInternal($sourceInstance, $relationshipInstance, $effectiveDate);

			// $stopwatch->stop();
			// trigger_error("MADI debug: exiting " . get_class($this) . "::getRelatedInstancesInternal (took " . $stopwatch->getElapsedTime() . "s)");

			return $insts;
		}
		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);
			if ($instParent === null)
				return null;

			$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;
		}

		private $_instanceTextCache;

		public function getInstanceText($inst)
		{
			if ($inst === null)
				return "ERR: inst null";

			if ($this->_instanceTextCache === null)
			{
				$this->_instanceTextCache = array();
			}
			if (array_key_exists($inst->GlobalIdentifier->__toString(), $this->_instanceTextCache))
			{
				return $this->_instanceTextCache[$inst->GlobalIdentifier->__toString()];
			}

			$parentClass = $this->getParentClass($inst);
			if ($parentClass === null)
			{
				$this->MetadataExceptions[] = new OmsMetadataException("Parent class empty", array
				(
					"For Instance" => $inst
				));
				return "ERR: no parent class for 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->execute($context, $instancesLabeledByRAMB);
				$text = $context->getWorkData($textAttr);

				//$this->printWorkData($context->workData);
				$this->_instanceTextCache[$inst->GlobalIdentifier->__toString()] = $text;
				return $text;
			}
			else
			{
				//echo ("no RAMB");
			}
			return $this->getAttributeValue($inst, $this->getInstanceByGlobalIdentifier(KnownAttributeGuids::Name));
		}

		public function removeRelationshipInstances(InstanceReference|string|InstanceKey $sourceInstance, InstanceReference|string|InstanceKey $relationshipInstance)
		{
			$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->getRelatedInstances($sourceInstance, $relationshipInstance);
			$this->removeRelationshipInstance($sourceInstance, $relationshipInstance, $targetInstances);
		}

		protected abstract function removeRelationshipInstanceInternal(InstanceReference|string|InstanceKey $sourceInstance, InstanceReference|string|InstanceKey $relationshipInstance, InstanceReference|string|InstanceKey|array $targetInstances, ?\DateTime $effectiveDate = null);
		public function removeRelationshipInstance(InstanceReference|string|InstanceKey $sourceInstance, InstanceReference|string|InstanceKey $relationshipInstance, InstanceReference|string|InstanceKey|array $targetInstances, ?\DateTime $effectiveDate = null)
		{
			return $this->removeRelationshipInstanceInternal($sourceInstance, $relationshipInstance, $targetInstances, $effectiveDate);
		}

		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() : ?TenantReference
		{
			return $this->_tenant;
		}
		public function setTenant(TenantReference $tenant) : void
		{
			$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 getInstanceKeysString(array $instanceReferences, string $separator = ":")
		{
			$val = "";
			$ct = count($instanceReferences);
			for($i = 0; $i < $ct; $i++)
			{
				$val .= $instanceReferences[$i]->InstanceKey;
				if ($i < $ct - 1)
				{
					$val .= $separator;
				}
			}
			return $val;
		}
		public function printInstanceKeys(array $instanceReferences, string $separator = ":")
		{
			echo ($this->getInstanceKeysString($instanceReferences, $separator));
		}

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

		public function getInstancesOf(InstanceReference|string|InstanceKey|UUID $classInstance) : ?array
		{
			$classInstance = $this->normalizeInstanceReference($classInstance);
			$instances = $this->getRelatedInstances($classInstance, KnownRelationshipGuids::Class__has__Instance);
			return $instances;
		}

		/**
		 * 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($value, $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($value), KnownAttributeGuids::Name);
						$expectedTypeName = $this->getAttributeValue($validClass, KnownAttributeGuids::Name);
						return false;
					}

					// this work set doesn't care whether or not $instWorkData passes validation
					return true;
				}
				else if ($this->is_a($instWorkData, KnownClassGuids::Clasz))
				{
					// Work Data is a Class, so make sure the value is of that class
					if (!$this->is_a($value, $instWorkData))
					{
						$actualTypeName = $this->getAttributeValue($this->getParentClass($value), KnownAttributeGuids::Name);
						$expectedTypeName = $this->getAttributeValue($instWorkData, KnownAttributeGuids::Name);
						return false;
					}
					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 buildAccessKeyForOmsAttachment($fileInstance, $entropy)
		{
			/*
			if (!entropy.ContainsKey(this.GetInstanceKey(inst)))
			{
				byte[] entropyData = new byte[256];
				random.NextBytes(entropyData);

				entropy[this.GetInstanceKey(inst)] = entropyData;
			}

			string entropy_co = Convert.ToBase64String(entropy[this.GetInstanceKey(inst)]);
			string guid = inst.GlobalIdentifier.ToString();
			long timestamp = DateTime.Now.Ticks;
			string originalFileName = this.GetAttributeValue<string>(inst, KnownAttributeGuids.Text.Name);
			string ft_co = String.Format("{0}?oms-attachments/{1}?{2}??{3}", entropy_co, guid, timestamp, originalFileName);
			string base64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(ft_co));
			string safeBase64 = MBS.Framework.Conversion.Base64ToUrlSafeBase64(base64);
			return safeBase64;
			*/

			$entropy_co = base64_encode("lalalala");
			//$timestamp = (new \DateTime())->
			$timestamp = "112340340";
			$originalFileName = "lalala.png";
			$ft_co = $entropy_co . "?oms-attachments/" . $fileInstance->GlobalIdentifier . "?" . $timestamp . "??" . $originalFileName;
			return base64_encode($ft_co);
		}

		public function getStackTrace(array $arr)
		{
			$sr = "";
			$ct = count($arr);
			for ($i = 0; $i < $ct; $i++)
			{
				if ($arr[$i] !== null)
				{
					$sr .= $arr[$i]->InstanceKey->__toString();
				}
				if ($i < $ct - 1)
				{
					$sr .= ":";
				}
			}
			return $sr;
		}

		private function createHtmlLinkToInstanceReference($val)
		{
			if ($val === null)
			{
				return "";
			}
			return "<a href=\"" . System::ExpandRelativePath("~/d/inst/" . $val->InstanceKey . ".htmld") . "\">" . $val->InstanceKey . "</a>";
		}
		public function getStackTraceWithLinks(array $arr)
		{
			$sr = "";
			$ct = count($arr);
			for ($i = 0; $i < $ct; $i++)
			{
				if ($arr[$i] !== null)
				{
					$sr .= $this->createHtmlLinkToInstanceReference($arr[$i]);
				}
				if ($i < $ct - 1)
				{
					$sr .= ":";
				}
			}
			return $sr;
		}

		public function getAttachmentUrl(InstanceReference $fileInstance)
		{
			$entropy = null;
			$accessKey = $this->buildAccessKeyForOmsAttachment($fileInstance, $entropy);
			return "/" . $this->getTenantName() . "/attachment/" . $fileInstance->InstanceKey . "/" . $accessKey;
		}

		public function executeReturningAttribute(OmsContext $context, InstanceReference $inst)
		{
			$value = $this->execute($context, $inst);
			if ($value !== null)
			{
				$val = $context->getWorkData($value);
				return $val;
			}
			return null;
		}

		public function executeAndEvaluateWorkData(OmsContext $context, InstanceReference $inst)
		{
			$result = $this->execute($context, $inst);
			if ($this->is_a($result, KnownClassGuids::WorkSet))
			{
				return $context->getWorkData($result);
			}
			return $result;
		}

		public function execute(OmsContext $context, InstanceReference $inst, $classesLookupWorkData = true, ?array $parms = null)
		{
			$instParent = $this->getParentClass($inst);
			$methodImplementation = $this->getMethodImplementation($instParent);
			if ($methodImplementation !== null)
			{
				$retval = $methodImplementation->execute($this, $context, $inst, $parms); // $this->executeMethod($context, $inst, $parms);
			}
			else if ($instParent->GlobalIdentifier->__is_equal(UUID::parse(KnownClassGuids::ReturnAttributeMethodBinding))
				|| ($instParent->GlobalIdentifier->__is_equal(UUID::parse(KnownClassGuids::ReturnInstanceSetMethodBinding)))
				|| ($instParent->GlobalIdentifier->__is_equal(UUID::parse(KnownClassGuids::ProcessUpdatesMethodBinding))))
			{
				$methodBindingExecutesMethod = $this->getRelatedInstance($inst, KnownRelationshipGuids::Method_Binding__executes__Method);
				if ($methodBindingExecutesMethod === null)
				{
					echo ("Method Binding.executes Method[" . KnownRelationshipGuids::Method_Binding__executes__Method . "] not found for " . $inst->InstanceKey);die();
				}

				$parameterAssignments = $this->getRelatedInstances($inst, KnownRelationshipGuids::Method_Binding__has__Parameter_Assignment);
				foreach ($parameterAssignments as $parameterAssignment)
				{
					$this->applyParameterAssignment($context, $parameterAssignment);
				}
				/*
				if (is_array(value: $parms))
				{
					foreach ($parms as $gid => $value)
					{
						$context->setWorkData($gid, $value);
					}
				}
				*/
				// echo ("MB stack: " . $methodInstance->InstanceKey . ":"  . $methodBindingExecutesMethod->InstanceKey);
				$retval = $this->execute($context, $methodBindingExecutesMethod, $parms);
			}
			else if (
				// $this->is_a($inst, UUID::parse(KnownClassGuids::TextAttribute))
				//|| $this->is_a($inst, UUID::parse(KnownClassGuids::NumericAttribute))
				//|| $this->is_a($inst, UUID::parse(KnownClassGuids::DateAttribute))
				// || $this->is_a($inst, UUID::parse(KnownClassGuids::BooleanAttribute))
				$this->is_a($inst, UUID::parse(KnownClassGuids::WorkSet))
				|| ($this->is_a($inst, UUID::parse(KnownClassGuids::Clasz)) && $classesLookupWorkData)
				)
			{
				//return $context->getWorkData($inst);
				$retval = $context->getWorkData($inst);
			}
			else
			{
				trigger_error("execute: unsupported class '" . $instParent->GlobalIdentifier . "' [" . $instParent->InstanceKey . "]");
				$retval = $inst;
			}
			return $retval;
		}

		public function applyParameterAssignment(OmsContext $context, InstanceReference $parameterAssignment)
		{
			$evaluateWorkData = $this->getAttributeValue($parameterAssignment, KnownAttributeGuids::EvaluateWorkSet, false);
			$assignsFromWorkData = $this->getRelatedInstance($parameterAssignment, KnownRelationshipGuids::Parameter_Assignment__assigns_from__Executable_returning_Work_Data);
			$assignsToParm = $this->getRelatedInstance($parameterAssignment, KnownRelationshipGuids::Parameter_Assignment__assigns_to__Work_Data);
			
			if ($assignsFromWorkData !== null && $assignsToParm !== null)
			{
				$workData = $this->execute($context, $assignsFromWorkData); 
				if ($workData !== null)
				{
					$workDataValue = $context->getWorkData($workData);
					// $context->getWorkData($assignsFromWorkData); //
					if (
						is_string($workDataValue) || 
						$this->is_a($workData, KnownClassGuids::WorkSet)
						)
					{
						// !! HACK !!
						$workData = $workDataValue;
					}
					
					//echo ("update with " . $assignsToParm->InstanceKey . " = " . $workDataValue);
					$context->setWorkData($assignsToParm, $workData);
				}
				else
				{
					// echo ("setting null on " . $assignsToParm->InstanceKey);
					$context->setWorkData($assignsToParm, $assignsFromWorkData);
				}
			}
		}

		public function evaluateConditions(OmsContext $context, ?array $trueConditions, ?array $falseConditions)
		{
			if ($trueConditions !== null)
			{
				foreach ($trueConditions as $condition)
				{
					$val = $this->executeReturningAttribute($context, $condition);
					if (!$val)
					{
						return false;
					}
				}
			}
			if ($falseConditions !== null)
			{
				foreach ($falseConditions as $condition)
				{
					$val = $this->executeReturningAttribute($context, $condition);
					if ($val)
					{
						return false;
					}
				}
			}
			return true;
		}

		public function printWorkData(array $workData)
		{
			foreach ($workData as $key => $value)
			{
				echo ($key . " == ");
				if ($value instanceof InstanceReference)
				{
					echo ("[" . $this->createHtmlLinkToInstanceReference($value) . "]");
				}
				else if (is_array($value))
				{
					echo("[ ");
					foreach ($value as $val)
					{
						if ($val instanceof InstanceReference)
						{
							echo ("[" . $this->createHtmlLinkToInstanceReference($value) . "]");
						}
						else
						{
							echo ("'" . $val . "'");
						}
						echo (", ");
					}
					echo (" ]");
				}
				else
				{
					echo ("'" . $value . "'");
				}
				echo (" ; ");
			}
		}

		public function CompareInstanceSets_ExactMatch(InstanceReference|array|null $left, InstanceReference|array|null $right)
		{
			//echo ("CIS: " . $left->InstanceKey . " ?? " . $right->InstanceKey);
			if ($left === null && $right === null)
				return true;
			if ($left === null || $right === null)
				return false;

			if (is_array($left) && is_array($right))
			{
				// Both are Instance Sets, so we can make some optimizations
				if (count($left) !== count($right))
					return false;

				$is = array_intersect($left, $right);
				return count($is) === count($left);
			}
			else
			{
				// Instance Set and Instance cannot be compared, for now
				if (is_array($left) || is_array($right))
				{
					return false;
				}
			}

			// If we get here, $left and $right should both be single instances
			if ($left->GlobalIdentifier->__is_equal($right->GlobalIdentifier))
			{
				return true;
			}
			return false;
		}

		private function __is_a_impl(InstanceReference $inst, InstanceReference $what, int $reentrant_counter)
		{
			if ($reentrant_counter > 10)
			{
				if ($reentrant_counter == 11)
				{
					echo("recursion detected!\n");
				}
				echo("__is_a_impl!(" . $inst->InstanceKey . ", " . $what->InstanceKey . ")\n");
				if ($reentrant_counter > 30)
				{
					die();
				}
			}

			$pclass = $this->getParentClass($inst);
			if ($pclass === null)
				return false;

			if ($pclass->__is_equal($what))
			{
				return true;
			}
			
			if (!($pclass->InstanceKey->ClassIndex == 1 && $pclass->InstanceKey->InstanceIndex == 1))
			{
				$pclassSuperclasses = $this->getRelatedInstances($pclass, KnownRelationshipGuids::Class__has_super__Class);
				foreach ($pclassSuperclasses as $pclassSuperclass)
				{
					if ($what->GlobalIdentifier->__is_equal($pclassSuperclass->GlobalIdentifier))
					{
						return true;
					}
				}
			}
			return false;
		}

		public function is_a(InstanceReference|string|InstanceKey|array|null $inst, InstanceReference|string|InstanceKey $what)
		{
			if ($inst === null)
			{
				return false;
			}
			if (is_array($inst))
			{
				return false;
			}
			$inst = $this->normalizeInstanceReference($inst);
			$what = $this->normalizeInstanceReference($what);

			return $this->__is_a_impl($inst, $what, 0);
		}

		public function setSessionAttributeValue(OmsContext $context, InstanceReference $instance, mixed $value)
		{
			$context->setTemporaryVariable($instance, $value);
		}
		public function getSessionAttributeValue(OmsContext $context, InstanceReference $instance, mixed $defaultValue = null)
		{
			return $context->getTemporaryVariable($instance, $defaultValue);
		}
	}
?>