#---------------------------------------------------------------------------
 #
 #  Title                : EMESARY multiplayer bridge
 #
 #  File Type            : Implementation File
 #
 #  Description          : Bridges selected emesary notifications over MP
 #                       : To send a message use a Transmitter with an object. That's all there is to it.
 #  
 #  References           : http://chateau-logic.com/content/emesary-nasal-implementation-flightgear
 #
 #  Author               : Richard Harrison (richard@zaretto.com)
 #
 #  Creation Date        : 04 April 2016
 #
 #  Version              : 4.8
 #
 #  Copyright © 2016 Richard Harrison           Released under GPL V2
 #
 #---------------------------------------------------------------------------*/

 # Example of connecting an incoming and outgoing bridge (should reside inside an aircraft nasal file)
 # 
 # var routedNotifications = [notifications.TacticalNotification.new()];
 # var incomingBridge = emesary_mp_bridge.IncomingMPBridge.startMPBridge(routedNotifications);
 # var outgoingBridge = emesary_mp_bridge.OutgoingMPBridge.new("F-15mp",routedNotifications);
 #------------------------------------------------------------------
 #
# NOTES:
#        * Aircraft do not need to have both an incoming and outgoing bridge, but it is usual.
#
#        * Only the notifications specified will be routed via the bridge.
#
#        * Once routed a message will by default not be re-rerouted again by the outgoing bridge.
#
#        * Transmit frequency and message lifetime may need to be tuned.
#
#        * IsDistinct messages must be absolute and self contained as a later message will 
 #        supercede any earlier ones in the outgoing queue (possibly prior to receipt)
#
#        * Use the message type and ident to identify distinct messages
#
#        * The outgoing 'port' is a multiplay/emesary/bridge index, however any available string property
 #        can be used by specifying it in the construction of the incoming or outgoing bridge.
 #        NOTE: This should not often be changed as it different versions of FG or model will  
 #              have to use the same properties to be able to communicate
#
#        * multiplay/emesary/bridge-type is used to identify the bridge that is in use. This is to 
 #        protect against bridges being used for different purposes by different models.
#
#        * The bridge-type property should contain an ID that identifies the purpose
 #        and thereore the message set that the bridge will be using.
#
 #  - ! is used as a seperator between the elements that are used to send the 
 #       notification (typeid, sequence, notification)
 #  -   There is an extra ! at the start of the message that is used to indicate protocol version.
 #  - ; is used to seperate serialzied elemnts of the notification

 # General Notes
 #----------------------------------------------------------------------
 # Outgoing messages are sent in a scheduled manner, usually once per
 # second, and each message has a lifetime (to allow for propogation to
 # all clients over UDP). Clients will ignore messages that they have
 # already received (based on the sequence id).

 # The incoming bridge will usually be created part of the aircraft
 # model file; it is important to understand that each AI/MP model will
 # have an incoming bridge as each element in /ai/models needs its own
 # bridge to keep up with the incoming sequence id. This scheme may not
 # work properly as it relies on the model being loaded which may only
 # happen when visible so it may be necessary to track AI models in a
 # seperate instantiatable incoming bridge manager.
 #
 # The outgoing bridge would usually be created within the aircraft loading Nasal.
 var EmesaryMPBridgeDefaultPropertyIndex=19;

 var OutgoingMPBridge = 
   {
    SeperatorChar : "!",
    MessageEndChar : "~",
    StartMessageIndex : 11,
    DefaultMessageLifetimeSeconds : 10, 
  MPStringMaxLen: 128,

  new: func(_ident, _notifications_to_bridge=nil, _mpidx=19, _root="", _transmitter=nil, _propertybase="emesary/bridge")
    {
        if (_transmitter == nil)
          _transmitter = emesary.GlobalTransmitter;

        print("OutgoingMPBridge created for "~_ident," mp=",_mpidx);
        var new_class = emesary.Recipient.new("OutgoingMPBridge "~_ident);


        if (_notifications_to_bridge == nil)
          new_class.NotificationsToBridge = [];
        else
          new_class.NotificationsToBridge = _notifications_to_bridge;

        new_class.NotificationsToBridge_Lookup = {};

        foreach (var n ; new_class.NotificationsToBridge) {
            print("  ",_ident,"  outwards bridge[",n,"] notifications of type --> ",n.NotificationType, " Id ",n.TypeId);
            n.MessageIndex = OutgoingMPBridge.StartMessageIndex;
            new_class.NotificationsToBridge_Lookup[n.TypeId] = n;
        }
        new_class.MPout = "";
        new_class.MPidx = _mpidx;
        new_class.MessageLifeTime = 10; # seconds
        new_class.OutgoingList = [];
        new_class.Transmitter = _transmitter;
        new_class.TransmitRequired=0;
        new_class.MpVariable = _root~"sim/multiplay/"~_propertybase~"["~new_class.MPidx~"]";
        new_class.TransmitterActive = 0;
        new_class.TransmitFrequencySeconds = 1;
        new_class.trace = 0;
        new_class.MPStringMaxLen = OutgoingMPBridge.MPStringMaxLen;

        new_class.TransmitTimer = 
          maketimer(6, func
                    {
                        if (new_class.TransmitterActive)
                          new_class.Transmit();
                        else
                          new_class.TransmitEnd();

                        new_class.TransmitTimer.restart(new_class.TransmitFrequencySeconds);
                    });

        new_class.Delete = func
          {
              if (me.Transmitter != nil) {
                  me.Transmitter.DeRegister(me);
                  me.Transmitter = nil;
              }
          };
        new_class.AddMessage = func(m)
        {
            append(me.NotificationsToBridge, m);
                                };

        #-------------------------------------------
        # Receive override:
        new_class.Receive = func(notification)
          {
              if (notification.FromIncomingBridge)
                return emesary.Transmitter.ReceiptStatus_NotProcessed;

              #print("Receive ",notification.NotificationType," ",notification.Ident);
              for (var idx = 0; idx < size(me.NotificationsToBridge); idx += 1) {
                  if (me.NotificationsToBridge[idx].NotificationType == notification.NotificationType) {
                      me.NotificationsToBridge[idx].MessageIndex += 1;
                      notification.MessageExpiryTime = systime()+me.MessageLifeTime;
                      notification.Expired = 0;
                      notification.BridgeMessageId = me.NotificationsToBridge[idx].MessageIndex;
                      notification.BridgeMessageNotificationTypeId = me.NotificationsToBridge[idx].TypeId;
                      #
                      # The message key is a composite of the type and ident to allow for multiple senders
                      # of the same message type.
                      #print("Received ",notification.BridgeMessageNotificationTypeKey," expire=",notification.MessageExpiryTime);
                      me.AddToOutgoing(notification);
                      return emesary.Transmitter.ReceiptStatus_Pending;
                  }
              }
              return emesary.Transmitter.ReceiptStatus_NotProcessed;
          };

        new_class.AddToOutgoing = func(notification)
          {
              if (notification.IsDistinct) {
                  for (var idx = 0; idx < size(me.OutgoingList); idx += 1) {
                      if (me.trace)
                        print("Compare [",idx,"] qId=",me.OutgoingList[idx].GetBridgeMessageNotificationTypeKey() ," noti --> ",notification.GetBridgeMessageNotificationTypeKey());
                      if (me.OutgoingList[idx].GetBridgeMessageNotificationTypeKey() == notification.GetBridgeMessageNotificationTypeKey()) {
                          if (me.trace)
                            print("  --> Update ",me.OutgoingList[idx].GetBridgeMessageNotificationTypeKey() ," noti --> ",notification.GetBridgeMessageNotificationTypeKey());
                          me.OutgoingList[idx]= notification;
                          me.TransmitterActive = size(me.OutgoingList);
                          return;
                      }
                  }
              } else
                if (me.trace)
                  print("Not distinct, adding always");
            
            if (me.trace)
              print(" --> Added ",notification.GetBridgeMessageNotificationTypeKey());
            append(me.OutgoingList, notification);
            me.TransmitterActive = size(me.OutgoingList);
        };
        new_class.Transmit = func
        {
            var outgoing = "";
            var cur_time=systime();
            var first_time = 1;
            me.OutgoingListNew = [];
            for (var idx = 0; idx < size(me.OutgoingList); idx += 1) {
                var sect = "";
                var notification = me.OutgoingList[idx];

                if (!notification.Expired and notification.MessageExpiryTime > cur_time) {
                    if (notification["sect"] == nil) {
                        # This is first time attempting to transmit this notification
                        var encval="";
                        
                        var eidx = 0;
                        notification.Expired = 0;

                        foreach(var p ; notification.bridgeProperties()) {
                            var nv = p.getValue();
                            encval = encval ~ nv;
                            eidx += 1;
                        }
                        #               !idx!typ!encv~
                        sect = sprintf("%s%s%s%s%s%s%s",
                                       OutgoingMPBridge.SeperatorChar, emesary.BinaryAsciiTransfer.encodeInt(notification.BridgeMessageId,4), 
                                       OutgoingMPBridge.SeperatorChar, emesary.BinaryAsciiTransfer.encodeInt(notification.BridgeMessageNotificationTypeId,1),
                                       OutgoingMPBridge.SeperatorChar, encval, OutgoingMPBridge.MessageEndChar);
                    } else {
                        # This notification has already been coded, but was previously not sent due to too little space.
                        sect = notification.sect;
                    }
                    if (size(outgoing) + size(sect) < me.MPStringMaxLen) {
                        outgoing = outgoing~sect;
                    } else {
                        if (first_time) {
                            print("Emesary: ERROR [",me.Ident,"] out of space for ",notification.NotificationType, " transmitted count=",idx, " queue size ",size(me.OutgoingList));
                            first_time = 0;
                        }
                        #notification.MessageExpiryTime = systime()+me.MessageLifeTime;
                        #break;
                        notification.sect = sect;
                        append(me.OutgoingListNew, notification);
                    }
                } else {
                    notification.Expired = 1;
                }
            }
            me.OutgoingList = me.OutgoingListNew;
            me.TransmitterActive = size(me.OutgoingList);
            setprop(me.MpVariable,outgoing);
        };
        new_class.TransmitEnd = func
        {
            if (getprop(me.MpVariable) != "") {
                setprop(me.MpVariable,"");
            }
        };
        new_class.Transmitter.Register(new_class);
        new_class.TransmitTimer.restart(new_class.TransmitFrequencySeconds);
        return new_class;
    },

   };


 #
 #
 # one of these for each model instantiated in the model XML - it will
 # route messages to 
 var IncomingMPBridge = 
   {
    trace : 0,
  new: func(_ident, _notifications_to_bridge=nil, _mpidx=19, _transmitter=nil, _propertybase="emesary/bridge")
    {
        if (_transmitter == nil)
          _transmitter = emesary.GlobalTransmitter;

        print("IncomingMPBridge created for "~_ident," mp=",_mpidx, " using Transmitter ",_transmitter.Ident, " with property base sim/multiplayer/"~_propertybase);

        var new_class = emesary.Transmitter.new("IncominggMPBridge "~_ident);

        if (_notifications_to_bridge == nil)
          new_class.NotificationsToBridge = [];
        else
          new_class.NotificationsToBridge = _notifications_to_bridge;

        new_class.NotificationsToBridge_Lookup = {};

        foreach (var n ; new_class.NotificationsToBridge) {
            print("  Incoming bridge notification type --> ",n.NotificationType);
            var new_n = {parents: [n]};
            new_n.IncomingMessageIndex = OutgoingMPBridge.StartMessageIndex;
            new_class.NotificationsToBridge_Lookup[n.TypeId] = new_n;
        }

        new_class.MPout = "";
        new_class.MPidx = _mpidx;
        new_class.MPpropertyBase = _propertybase;
        new_class.MessageLifeTime = OutgoingMPBridge.DefaultMessageLifetimeSeconds; # seconds
        new_class.OutgoingList = [];
        new_class.Transmitter = _transmitter;
        new_class.MpVariable = "";

        new_class.Connect = func(_root)
        {
            me.MpVariable = _root~"sim/multiplay/"~new_class.MPpropertyBase~"["~new_class.MPidx~"]";
            me.CallsignPath = _root~"callsign";
            me.PropertyRoot = _root;
            me.mp_listener = setlistener(me.MpVariable, func(v)
                        {
#print("incoming ",getprop(me.CallsignPath)," -->",me.PropertyRoot," v=",v.getValue());
                            me.ProcessIncoming(v.getValue());
                        },0,0);
          };
        new_class.setprop = func(property, value){
            if (IncomingMPBridge.trace == 2)
              print("setprop ",new_class.PropertyRoot~property," = ",value);
            setprop(new_class.PropertyRoot~property,value);
        };
        new_class.GetCallsign = func
          {
              return getprop(me.CallsignPath);
          };
        new_class.AddMessage = func(m)
        {
            append(me.NotificationsToBridge, m);
                                };

        new_class.Remove = func
        {
            print("Emesary IncomingMPBridge Remove() ",me.Ident," Property: ",me.MpVariable);
            me.Transmitter.DeRegister(me);
            if (me["mp_listener"] != nil)
                removelistener(me.mp_listener);
            me.mp_listener = nil;
        };

        #-------------------------------------------
        # Receive override:
        new_class.ProcessIncoming = func(encoded_val)
          {
              if (encoded_val == "")
                return;

            if(right(encoded_val,1) != OutgoingMPBridge.MessageEndChar) 
              printf("Error: emesary.IncomingBridge.ProcessIncoming Missing endChar. From %s. Message=%s",me.GetCallsign(),encoded_val);

              var encoded_notifications = split(OutgoingMPBridge.MessageEndChar, encoded_val);
              for (var idx = 0; idx < size(encoded_notifications); idx += 1) {
                  if (encoded_notifications[idx] == "")
                    continue ;
                  # get the message parts
                  var encoded_notification = split(OutgoingMPBridge.SeperatorChar, encoded_notifications[idx]);
                  if (size(encoded_notification) < 4)
                    print("Error: emesary.IncomingBridge.ProcessIncoming bad msg ",encoded_notifications[idx]);
                  else {
                      var msg_idx = emesary.BinaryAsciiTransfer.decodeInt(encoded_notification[1],4,0).value;
                      var msg_type_id = emesary.BinaryAsciiTransfer.decodeInt(encoded_notification[2],1,0).value;
                      var bridged_notification = new_class.NotificationsToBridge_Lookup[msg_type_id];
                      if (bridged_notification == nil) {
                          print("Error: emesary.IncomingBridge.ProcessIncoming invalid type_id ",msg_type_id);
                      } else {
                          bridged_notification.FromIncomingBridge = 1;
                          bridged_notification.Callsign = me.GetCallsign();
                            if(IncomingMPBridge.trace>1)
                              print("ProcessIncoming callsign=",bridged_notification.Callsign," ",me.PropertyRoot, " msg_type=",msg_type_id," idx=",msg_idx, " bridge_idx=",bridged_notification.IncomingMessageIndex);
                        if (msg_idx > bridged_notification.IncomingMessageIndex) {
                            if(IncomingMPBridge.trace==1)
                              print("ProcessIncoming callsign=",bridged_notification.Callsign," ",me.PropertyRoot, " msg_type=",msg_type_id," idx=",msg_idx, " bridge_idx=",bridged_notification.IncomingMessageIndex);
                              var msg_body = encoded_notification[3];
                              if (IncomingMPBridge.trace > 2)
                                print("received idx=",msg_idx," ",msg_type_id,":",bridged_notification.NotificationType);

                              # populate fields
                              var bridgedProperties = bridged_notification.bridgeProperties();
                              var msglen = size(msg_body);
                              if (IncomingMPBridge.trace > 2)
                                print("Process ",msg_body," len=",msglen, " BPsize = ",size(bridgedProperties));
                              var pos = 0;
                              for (var bpi = 0; bpi < size(bridgedProperties); bpi += 1) {
                                  if (pos < msglen) {
                                      if (IncomingMPBridge.trace > 2)
                                        print("dec: pos ",pos);
                                      var bp = bridgedProperties[bpi];
                                      dv = bp.setValue(msg_body, me, pos);
                                      if (IncomingMPBridge.trace > 2)
                                      print(" --> next pos ", dv.pos);

                                      if (dv.pos == pos or dv.pos > msglen)
                                        break;
                                      pos = dv.pos;
                                } else {
                                    print("Error: emesary.IncomingBridge.ProcessIncoming: [",bridged_notification.NotificationType,"] supplementary encoded value at position ",bpi);
                                    break;
                                }
                            }
                            # maybe extend the bridge to allow certain notifications to only be routed to a specific player;
                            # i.e. 
                            # (notification.Callsign == nil or notification.Callsign == getprop("/sim/multiplay/callsign"))

                            if (bridged_notification.Ident == "none")
                              bridged_notification.Ident = "mp-bridge";
                              
                            bridged_notification.IncomingMessageIndex = msg_idx;
                            me.Transmitter.NotifyAll(bridged_notification);
                        }
                    }
                }
            }
        }
        foreach (var n; new_class.NotificationsToBridge) {
            print("IncomingBridge: ",n.NotificationType);
        }
        return new_class;
    },
    connectIncomingBridge : func(path, notification_list, mpidx, transmitter, _propertybase){
        var incomingBridge = emesary_mp_bridge.IncomingMPBridge.new(path, notification_list, mpidx, transmitter, _propertybase);
        
        incomingBridge.Connect(path~"/");
        if (me.incomingBridgeList[path] == nil) {
            me.incomingBridgeList[path] = [incomingBridge];
        } else {
            append(me.incomingBridgeList[path],incomingBridge);
        }
        return incomingBridge;
    },

    #
    # Each multiplayer object will have its own incoming bridge. This is necessary to allow message ID
    # tracking (as the bridge knows which messages have been already processed)
    # Whenever a client connects over MP a new bridge is instantiated
    startMPBridge : func(notification_list, mpidx=19, transmitter=nil, _propertybase="emesary/bridge") 
    {
        me.incomingBridgeList = {};

        #
        # Create bridge whenever a client connects
        #
        setlistener("/ai/models/model-added", func(v)
                    {
                        # Model added will be eg: /ai[0]/models[0]/multiplayer[0]
                        var path = v.getValue();

                        # Ensure we only handle multiplayer elements
                        if (find("/multiplayer",path) > 0) {
                            var callsign = getprop(path~"/callsign");
                            print("Creating Emesary MPBridge for ",path);
                            if (callsign == "" or callsign == nil)
                              callsign = path;

                me.connectIncomingBridge(path, notification_list, mpidx, transmitter, _propertybase);
                        }
        });

        #
        # when a client disconnects remove the associated bridge.
        #
        setlistener("/ai/models/model-removed", func(v){
            var path = v.getValue();
            var bridges = me.incomingBridgeList[path];
            if (bridges != nil) {
                foreach(bridge;bridges) {
                    bridge.Remove();
#                   print("Bridge removed for ",v.getValue());
            }
                me.incomingBridgeList[path] = nil;
        }
        });
    },
   };