#---------------------------------------------------------------------------
 #
 #	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(nil)];
# 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("  Outward bridge notifications of type --> ",n.NotificationType);
              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.Transmitter.Register(new_class);
        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();

                        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();
            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)
                {
                    var encval="";
                    var first_time = 1;
                    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);

                    if (size(outgoing) + size(sect) < me.MPStringMaxLen)
                    {
                        outgoing = outgoing~sect;
                    }
                    else
                    {
                        print("Emesary: ERROR [",me.Ident,"] out of space for ",notification.NotificationType, " transmitted count=",idx, " queue size ",size(me.OutgoingList));
                        notification.MessageExpiryTime = systime()+me.MessageLifeTime;
                        break;
                    }
                }
                else
                {
                    notification.Expired = 1;
                }
            }
            me.TransmitterActive = size(me.OutgoingList);
            setprop(me.MpVariable,outgoing);
#            print("Set ",me.MpVariable," size(",size(outgoing));
#loopback test:
# incomingBridge.ProcessIncoming(outgoing);

#
# Now remove any expired messages from the outgoing queue.
# The only real way of doing this is via the pop() function
# so we have to delete the expired items from the list by rebuilding
# the list from the start with non-expired items, and then 
# all of the items past the end (of the rebuilt list) can be popped 
# (pop removes the last element from a vector)
            var outSize = size(me.OutgoingList)-1;
            var out_idx = 0;
            for (var idx = 0; idx <= outSize; idx += 1)
            {
#print("Q1 [",idx,"] ",me.OutgoingList[idx].MessageExpiryTime-cur_time," Expired=",me.OutgoingList[idx].Expired);
                if(!me.OutgoingList[idx].Expired)
                {
#print("move  ",idx, " => ",out_idx);
                    var mmove = me.OutgoingList[idx];
                    me.OutgoingList[out_idx] = me.OutgoingList[idx];
                    out_idx += 1;
                }
            }
            var to_del = (outSize+1) - out_idx;       
#print("--> out idx",out_idx, " to delete ",to_del);
            while(to_del > 0)
            {
#print("--> pop ");
                pop(me.OutgoingList);
                to_del = to_del - 1;
            }
        };
        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);
              new_class.NotificationsToBridge_Lookup[n.TypeId] = 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.IncomingMessageIndex = OutgoingMPBridge.StartMessageIndex;

            setlistener(me.MpVariable, func(v)
                        {
#print("incoming ",getprop(me.CallsignPath)," -->",me.PropertyRoot," v=",v.getValue());
                            me.ProcessIncoming(v.getValue());
                        });
        };
        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);
            me.Transmitter.DeRegister(me);
        };

        #-------------------------------------------
# Receive override:
        new_class.ProcessIncoming = func(encoded_val)
        {
            if (encoded_val == "")
              return;
            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)
                              print("ProcessIncoming ",bridged_notification.Callsign," ",me.PropertyRoot, "idx=",msg_idx, " bridge_idx=",me.IncomingMessageIndex);
                        if (msg_idx > me.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 ",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";
                            me.Transmitter.NotifyAll(bridged_notification);
                            me.IncomingMessageIndex = msg_idx;
                        }
                    }
                }
            }
        }
        foreach (var n; new_class.NotificationsToBridge) {
            print("IncomingBridge: ",n.NotificationType);
        }
        return new_class;
    }
 ,
    #
    # 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")
    {
        var 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;

                var incomingBridge = emesary_mp_bridge.IncomingMPBridge.new(path, notification_list, mpidx, transmitter, _propertybase);

                incomingBridge.Connect(path~"/");
                incomingBridgeList[path] = incomingBridge;
            }
        });

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