/// <summary>
 /// Sends a message with the given TXT and PLS/PLB to the target and returns the TXT of the response message separated by ':' into a string array.
 /// </summary>
 /// <param name="originator">Thing or engine to use for response messages. If NULL defaults to ContentService.</param>
 /// <param name="target">Thing or engine to which the message is to be sent.</param>
 /// <param name="messageName">The "command" of the message. This will be the first entry in the TXT separated by ':', i.e. messageName:correlationToken:txtParameter1:txtParameter2...</param>
 /// <param name="timeout">If the response message is not returned to the callback within this time, the callback will be invoked with a null parameter.</param>
 /// <param name="responseCallback">The method that will be called once the response message has been received or a timeout has occured.  The parameter will contain the array of parts of the response message's TXT.</param>
 /// <param name="txtParameters">The strings that will follow messageName and the correlation token in the TXT separated by ':', i.e. messageName:correlationToken:txtParameter1:txtParameter2...</param>
 /// <param name="PLS">The payload of the message as a string.</param>
 /// <param name="PLB">The payload of the message as a byte[].</param>
 public static void PublishRequestCallback(TheMessageAddress originator, TheMessageAddress target, string messageName, TimeSpan timeout, Action <string[]> responseCallback, string[] txtParameters = null, string PLS = null, byte[] PLB = null)
 {
     PublishRequestCallback(originator, target, messageName, timeout, (TSM msg) =>
     {
         ParseRequestOrResponseMessage(msg, out string[] responseParams, out Guid token);
         responseCallback(responseParams);
     }, txtParameters, PLS, PLB);
        static TimeSpan MaxTimeOut = new TimeSpan(1, 0, 0); // TODO Add to TheBaseAssets and make configurable?

        private static TSM PrepareRequestMessage(TheMessageAddress originator, TheMessageAddress target, string messageName, Guid correlationToken, string[] txtParameters, string PLS, byte[] PLB)
        {
            var parameterText = CreateParameterText(txtParameters);

            if (parameterText == null)
            {
                return(null);
            }

            TSM msg = new TSM(target.EngineName, String.Format("{0}:{1}:{2}", messageName, TheCommonUtils.cdeGuidToString(correlationToken), parameterText), PLS);

            if (PLB != null)
            {
                msg.PLB = PLB;
            }
            if (originator.ThingMID != Guid.Empty)
            {
                msg.SetOriginatorThing(originator.ThingMID);
            }
            if (target.ThingMID != Guid.Empty)
            {
                msg.OWN = TheCommonUtils.cdeGuidToString(target.ThingMID);
            }
            if (!string.IsNullOrEmpty(target.Route))
            {
                msg.GRO = target.Route;
            }
            return(msg);
        }
 /// <summary>
 /// Sends a TSM message to the targetThing and return the matching response message
 /// </summary>
 /// <param name="originator">Thing or engine to use for response messages. If NULL defaults to ContentService.</param>
 /// <param name="target">Thing or engine to which the message is to be sent.</param>
 /// <param name="messageName">Name of the message, as defined by the target thing</param>
 /// <param name="txtParameters">Array of simple string parameters, to be attached to the message's TXT field, as defined by the target thing. txtParameters must not contain ":" characters.</param>
 /// <param name="PLS">String payload to be set as the message's PLS field, as defined by the target thing.</param>
 /// <param name="PLB">Binary pauload to be set as the message's PLB field, as defined by the target thing.</param>
 /// <param name="timeout">Time to wait for the response message. Can not exceed TheBaseAsset.MaxMessageResponseTimeout (default: 1 hour).</param>
 /// <returns>The response message as defined by the target thing. The response parameters and correlation token can be parsed using ParseRequestOrResponseMessage, if the target thing used the PublishResponseMessage or an equivalent message format.
 /// If the message times out or an error occured while sending the message, the return value will be null.
 /// </returns>
 public static Task <ParsedMessage> PublishRequestAndParseResponseAsync(TheMessageAddress originator, TheMessageAddress target, string messageName, TimeSpan timeout, string[] txtParameters = null, string PLS = null, byte[] PLB = null)
 {
     return(PublishRequestAsync(originator, target, messageName, timeout, txtParameters, PLS, PLB).ContinueWith((t) =>
     {
         var msg = t.Result;
         return ParseRequestOrResponseMessage(msg);
     }));
 }
        private static object RegisterRequestCallback(TheMessageAddress originator, Action <ICDEThing, object> callback)
        {
            // TODO optimize engine handler registration/unregistration / caching? Better way to receive messages targetted to a thing?
            var originatorThing = originator.GetBaseThing();

            if (originatorThing != null)
            {
                originatorThing.RegisterEvent(eThingEvents.IncomingMessage, callback);
                return(originatorThing);
            }
            IBaseEngine originatorEngine = originator.GetBaseEngine();

            if (originatorEngine != null)
            {
                originatorEngine.RegisterEvent(eEngineEvents.IncomingMessage, callback);
                return(originatorEngine);
            }
            return(null);
        }
        static readonly TimeSpan defaultRequestTimeout = new TimeSpan(0, 1, 0); // TODO make this configurable?

        /// <summary>
        /// Sends a TSM message to the targetThing and return the matching response message
        /// </summary>
        /// <param name="originator">Thing or engine to use for response messages. If NULL defaults to ContentService.</param>
        /// <param name="target">Thing or engine to which the message is to be sent.</param>
        /// <param name="messageName">Name of the message, as defined by the target thing. The response message will be messageName_RESPONSE.</param>
        /// <param name="txtParameters">Array of simple string parameters, to be attached to the message's TXT field, as defined by the target thing. txtParameters must not contain ":" characters.</param>
        /// <param name="PLS">String payload to be set as the message's PLS field, as defined by the target thing.</param>
        /// <param name="PLB">Binary pauload to be set as the message's PLB field, as defined by the target thing.</param>
        /// <param name="timeout">Time to wait for the response message. Can not exceed TheBaseAsset.MaxMessageResponseTimeout (default: 1 hour).</param>
        /// <returns>The response message as defined by the target thing. The response parameters and correlation token can be parsed using ParseRequestOrResponseMessage, if the target thing used the PublishResponseMessage or an equivalent message format.
        /// If the message times out or an error occured while sending the message, the return value will be null.
        /// </returns>
        public static Task <TSM> PublishRequestAsync(TheMessageAddress originator, TheMessageAddress target, string messageName, TimeSpan timeout, string[] txtParameters = null, string PLS = null, byte[] PLB = null)
        {
            Guid correlationToken = Guid.NewGuid();

            if (originator == null)
            {
                originator = TheThingRegistry.GetBaseEngineAsThing(eEngineName.ContentService);
            }
            var msg = PrepareRequestMessage(originator, target, messageName, correlationToken, txtParameters, PLS, PLB);

            if (msg == null)
            {
                return(null);
            }

            if (timeout > MaxTimeOut)
            {
                timeout = MaxTimeOut;
            }
            if (timeout == TimeSpan.Zero)
            {
                timeout = defaultRequestTimeout;
            }

            var tcsResponse = new TaskCompletionSource <TSM>();

            var callback = new Action <ICDEThing, object>((tSenderThing, responseMsgObj) =>
            {
                var responseMsg = CheckResponseMessage(messageName, correlationToken, responseMsgObj);
                if (responseMsg != null)
                {
                    var bReceived = tcsResponse.TrySetResult(responseMsg);
                    if (bReceived)
                    {
                        TheSystemMessageLog.WriteLog(1000, TSM.L(eDEBUG_LEVELS.FULLVERBOSE) ? null : new TSM("Thing Registry", $"Processed response message for {messageName}", eMsgLevel.l2_Warning, $"{correlationToken}: {responseMsg?.TXT}"), false);
                    }
                    else
                    {
                        TheSystemMessageLog.WriteLog(1000, TSM.L(eDEBUG_LEVELS.FULLVERBOSE) ? null : new TSM("Thing Registry", $"Failed to process response message for {messageName}: timed out or double response", eMsgLevel.l2_Warning, $"{correlationToken}: {responseMsg?.TXT}"), false);
                    }
                }
                else
                {
                    responseMsg = (responseMsgObj as TheProcessMessage)?.Message;
                    TheSystemMessageLog.WriteLog(1000, TSM.L(eDEBUG_LEVELS.FULLVERBOSE) ? null : new TSM("Thing Registry", $"Ignoring response message for {messageName}", eMsgLevel.l2_Warning, $"{correlationToken}: {responseMsg?.TXT}"), false);
                }
            });

            var originatorThingOrEngine = RegisterRequestCallback(originator, callback);

            if (originatorThingOrEngine == null)
            {
                return(null);
            }

            if (target.SendToProvisioningService)
            {
                TheISMManager.SendToProvisioningService(msg);
            }
            else if (target.Node != Guid.Empty)
            {
                TheCommCore.PublishToNode(target.Node, msg);
            }
            else
            {
                TheCommCore.PublishCentral(msg, true);
            }

            TheCommonUtils.TaskWaitTimeout(tcsResponse.Task, timeout).ContinueWith((t) =>
            {
                UnregisterRequestCallback(originatorThingOrEngine, callback);
                var timedOut = tcsResponse.TrySetResult(null); // Will preserve value if actual result has already been set
                if (timedOut)
                {
                    TheSystemMessageLog.WriteLog(1000, TSM.L(eDEBUG_LEVELS.FULLVERBOSE) ? null : new TSM("Thing Registry", $"Timeout waiting for response message for {messageName}", eMsgLevel.l2_Warning, $"{correlationToken}"), false);
                }
            });
            return(tcsResponse.Task);
        }