#---------------------------------------------------------------------------
 #
 #	Title                : Emesary based 'real time' module executive
 #
 #	File Type            : Implementation File
 #
 #	Description          : Uses Emesary notifications to permit Nasal subsystems to be invoked in
 #                       : a controlled manner.
 #                       : 
 #                       : Sends out a FrameNotification for each frame recipient can implement
 #                       : workload reduction as appropriate based on skipping frames (2=half,
 #                       : 4=quarter etc.) because some code can safely be run at quarter rate
 #                       : (e.g. ~10hz).
 #                       : 
 #                       : The developer should interleave slower rate modules to spread out
 #                       : workload A frame is defined by the timer rate; which is usually the
 #                       : maximum rate as determined by the FPS.
 #                       : 
 #                       : This is an alternative to the timer based or explicit function calling
 #                       : way of invoking aircraft systems.  It has the advantage of using less
 #                       : timers and remaining modular, as each aircraft subsytem can simply
 #                       : register itself with the global transmitter to receive the frame
 #                       : notification.
 #
 #	See Also             : https://wiki.flightgear.org/Nasal_Optimisation#Emesary_real_time_executive
 #                       : F-15 and F-14 for examples of how to use this.
 #
 #	Author               : Richard Harrison (richard@zaretto.com)
 #
 #	Creation Date        : 4 June 2018
 #
 #  Copyright (C) 2018 Richard Harrison           Released under GPL V2
 #
 #---------------------------------------------------------------------------*/

#
#
# This is the notification that is sent out to all recipients each frame.
# The notification contains a hash of property values.
# Frame modules can request that the hash includes key/property pairs 
# by using the FrameNotificationAddProperty
#
# An instance of this class is be contained within the  EmesaryExecutive.
var FrameNotification = 
{
    debug: 0,
    # The rate and the transmitter to use
    new: func(_rate, transmitter=nil)
    {
        if (transmitter == nil)
            transmitter = emesary.GlobalTransmitter;

        var new_class = emesary.Notification.new("FrameNotification", _rate, 0);
        append(new_class.parents, FrameNotification);
        new_class.Rate = _rate;
        new_class.FrameCount = 0;
        new_class.ElapsedSeconds = 0;
        new_class.monitored = {};
        new_class.properties = {};
        new_class.transmitter = transmitter;

        #
        # embed a recipient within this notification to allow the monitored property
        # mapping list to be modified.
        new_class.Recipient = emesary.Recipient.new("FrameNotification");
        new_class.Recipient.Receive = func(notification)
        {
            if (notification.NotificationType == "FrameNotificationAddProperty")
            {
                var root_node = props.globals;
                if (notification.root_node != nil) {
                    root_node = notification.root_node;
                }
                if (new_class.properties[notification.property] != nil 
                    and new_class.properties[notification.property] != notification.variable)
                  logprint(1,"FrameNotification: (",notification.module,") FrameNotification: already have variable ",new_class.properties[notification.property]," for ",notification.variable, " referencing property ",notification.property);

                if (new_class.monitored[notification.variable] != nil 
                    and new_class.monitored[notification.variable].getPath() != notification.property
                    and new_class.monitored[notification.variable].getPath() != "/"~notification.property)
                  logprint(1,"FrameNotification: (",notification.module,") FrameNotification: already have variable ",notification.variable,"=",new_class.monitored[notification.variable].getPath(), " using different property ",notification.property);
                #                else if (new_class.monitored[notification.variable] == nil)
                #                  print("[INFO]: (",notification.module,") FrameNotification.",notification.variable, " = ",notification.property);

                new_class.monitored[notification.variable] = root_node.getNode(notification.property,1);
                new_class.properties[notification.property] = notification.variable;

                logprint(3,"(",notification.module,") FrameNotification.",notification.variable, " = ",notification.property, " -> ", new_class.monitored[notification.variable].getPath() );
                return emesary.Transmitter.ReceiptStatus_OK;
            }
            return emesary.Transmitter.ReceiptStatus_NotProcessed;
        };
        new_class.transmitter.Register(new_class.Recipient);
        return new_class;
    },
    fetchvars : func() {
        foreach (var mp; keys(me.monitored)){
            if(me.monitored[mp] != nil){
                if (FrameNotification.debug > 1)
                    logprint(4," ",mp, " = ",me.monitored[mp].getValue());
                me[mp] = me.monitored[mp].getValue();
            }
        }
    },
};

#
# request to add a property to the frame notification
var FrameNotificationAddProperty = 
{
    new: func(module, variable, property, root_node=nil)
    {
        var new_class = emesary.Notification.new("FrameNotificationAddProperty", variable, 0);
        if (root_node == nil)
          root_node = props.globals;
        new_class.module = module ;
        new_class.variable = variable;
        new_class.property = property;
        new_class.root_node = root_node;
        return new_class;
    },
};

#
# the main exeuctive class.
# There will be one of these as emexec.ExceModule however multiple instances could be 
# created - but only by those who understand scheduling - because it is not necessary
# to have more than one - unless we mange to enable some sort of per core threading.
var EmesaryExecutive =  {
    new : func(_ident="EMEXEC", transmitter=nil) {

        # by default use global transmitter
        if (transmitter == nil)
            transmitter = emesary.GlobalTransmitter;

        var new_class = {
            parents: [EmesaryExecutive],
            Ident: _ident,
            lp : aircraft.lowpass.new(3),
            frameNotification : FrameNotification.new(1, transmitter),
            frame_inc : 0,
            cur_frame_inc : 0.033, # start off at 33hz
        };
        new_class.transmitter = transmitter;
        
        # setup the properties to monitor for this system
        var exec_prop_list = {
                frame_rate                : "/sim/frame-rate",
                frame_rate_worst          : "/sim/frame-rate-worst",
                elapsed_seconds           : "/sim/time/elapsed-sec",
                };
        new_class.monitor_properties(exec_prop_list);

        # now setup the timer.
        # - initially use update rate of 30hz.
        # - use simulated time as otherwise will continue to be called when paused
        # - start timer now, as the listener will effectively block module calls until sim init
        new_class.frameNotification.running = 0;
        setlistener("sim/signals/fdm-initialized", func(v) {
            logprint(1,"started ",new_class.Ident);
            new_class.frameNotification.dT = 0; # seconds
            new_class.frameNotification.curT = 0;
            new_class.frameNotification.running = 1;
        });
        new_class.execTimer = maketimer(new_class.cur_frame_inc, new_class, new_class.timerCallback);
        new_class.execTimer.simulatedTime = 1;

        return new_class;
     },
     start : func {
        me.execTimer.start();
     },
     stop : func {
        me.execTimer.stop();
     },
     # request monitoring of a list of hash value pairs.
     monitor_properties : func(input){
        # this uses a notification to isolate the implementation which is also in this module; so it could
        # call directly; however the design is that a FrameNotification add property could also trigger other
        # logic that we do not know about.
        foreach (var name; keys(input)) {
            me.transmitter.NotifyAll(FrameNotificationAddProperty.new(me.Ident, name, input[name]));
        }
     },

     # ident: String (e.g F-15 HUD)
     # inputs: hash of properties to monitor
     #         : e.g 
     #           {
     #               AirspeedIndicatorIndicatedMach          : "instrumentation/airspeed-indicator/indicated-mach",
     #               Alpha                                   : "orientation/alpha-indicated-deg",
     #           }
     #          : object - must have an update(notification) method that will receive a  frame notification
     #          : rate is the frame skip update rate (1/update rate). 0 or 1 means full rate
     #          : offset is the offset to permit interleave
     #             - e.g for two objects to interleave we could have a rate of two and an offset of 0 and 1 which
     #                    would result in one object being processed per frame
     register: func(ident, properties_to_monitor, object, rate=1, frame_offset=0) {
		var new_class = emesary.Recipient.new(ident);

        me.monitor_properties(properties_to_monitor);
        new_class.object = object;
        new_class.Receive = func(notification)
        {
            if (notification.NotificationType == "FrameNotification"){
                if (rate <= 1 or 0 == math.mod(notification.FrameCount + frame_offset,rate)){
                    new_class.object.update(notification);
                }
                return emesary.Transmitter.ReceiptStatus_OK;
            }
            return emesary.Transmitter.ReceiptStatus_NotProcessed;
        };
        me.transmitter.Register(new_class);
        return new_class;
  },
  timerCallback : func {

         if (!me.frameNotification.running){
            logprint(3, me.Ident~": Waiting for start");
            return;
        }
        me.frameNotification.fetchvars();
        me.frameNotification.dT = me.frameNotification.elapsed_seconds - me.frameNotification.curT;

        if (me.frameNotification.dT > 1.0) 
        me.frameNotification.curT = me.frameNotification.elapsed_seconds;

        me.transmitter.NotifyAll(me.frameNotification);

        me.frameNotification.FrameCount = me.frameNotification.FrameCount + 1;
        me.frameNotification.filtered_frame_rate_worst = (int)(me.lp.filter(me.frameNotification.frame_rate_worst));
    
        # this permits us to go up to 1/32 rate (which could be less than 1hz)
        if (me.frameNotification.FrameCount > 32) {
            me.frameNotification.FrameCount = 0;
        # adjust exec rate based on frame rate.
            if (me.frameNotification.filtered_frame_rate_worst < 5) {
                me.frame_inc = 0.25;#4 Hz
            } elsif (me.frameNotification.filtered_frame_rate_worst < 10) {
                me.frame_inc = 0.125;#8 Hz
            } elsif (me.frameNotification.filtered_frame_rate_worst < 15) {
                me.frame_inc = 0.10;#10 Hz
            } elsif (me.frameNotification.filtered_frame_rate_worst < 20) {
                me.frame_inc = 0.075;#13.3 Hz
            } elsif (me.frameNotification.filtered_frame_rate_worst < 25) {
                me.frame_inc = 0.05;#20 Hz
            } elsif (me.frameNotification.filtered_frame_rate_worst < 40) {
                me.frame_inc = 0.0333;#30 Hz
            } else {
                me.frame_inc = 0.02;#50 Hz
            }

            # Adjust timer if new value
            if (me.frame_inc != me.cur_frame_inc) {
                logprint(3,  me.Ident~": Adjust frequency to ",1/me.frame_inc, " Hz");
                me.cur_frame_inc = me.frame_inc;
                me.execTimer.restart(me.cur_frame_inc);
            }
        }
    }
};

# profiling aid: embed within a class
# and each frame call log("something") to trace time
# e.g. to create using the default log level of INFO:
#    ot = OperationTimer.new("VSD");
# or for log level debug 
#    ot = OperationTimer.new("VSD",2);
# ...
# ot.reset();
# ot.log("start");
# ... code ...
# ot.log("half way");
# ... code ...
# ot.log("finished");

OperationTimer = {
    new : func (ident="timer", level=3) {
        {
            parents: [OperationTimer],
            timestamp: maketimestamp(),
            ident: ident,
            resolution_uS: 1000.0,
            level : level,
        }
    },
    log : func( text){
        logprint(me.level, sprintf("%10s: %8.3f : %s",me.ident,  me.timestamp.elapsedUSec()/me.resolution_uS, text));
    },
    reset : func {
       me.timestamp.stamp(); 
    }
};         




var xmit = emesary.Transmitter.new("exec");
var ExecModule =  EmesaryExecutive.new("EMEXEC", xmit);
ExecModule.start();