################################################################################
#
# Automated Checklists
#
# Copyright (c) 2015, Richard Senior
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
################################################################################
#
# This script runs the bindings in a sequence of checklists. Typically used
# to implement autostart or shutdown but can run any checklist sequence at any
# time. Checklist execution runs each binding where the condition is not yet
# satisfied and waits until the condition becomes true before moving on. If
# the condition does not become true within a timeout period, the checklist
# execution fails.
#
# Can also run in no wait mode where items are run in order without waiting
# for the previous condition to become true. Checklists never timeout or fail
# in no wait mode but some conditions may remain false.
#
# Typical usage:
#
# 1. Add this script to <Aircraft>-set.xml:
#
#    <nasal>
#      ... other scripts here
#      <autochecklist>
#        <file>Aircraft/Generic/autochecklist.nas</file>
#      </autochecklist>
#    </nasal>
#
# 2. In the <Aircraft>-set.xml, define a list of checklist indexes to run
#    as a named sequence.
#
#    For example, to run checklists with indexes 0 and 1 for startup and
#    checklist 9 for shutdown:
#
#   <checklists>
#     <checklist include="Checklists/before-starting-engines.xml"/>
#     <checklist include="Checklists/start-engines.xml"/>
#     ... more checklists here
#     <checklist include="Checklists/parking.xml"/>
#     <startup>
#       <index n="0">0</index> <!-- Before starting engines -->
#       <index n="1">1</index> <!-- Start engines -->
#     </startup>
#     <shutdown>
#       <index n="0">9</index> <!-- Parking -->
#     </startup>
#   </checklists>
#
# 3. Define a menu item that calls the complete_checklists function with
#    the name of the checklist sequence you would like to run.
#
#    For example:
#
#    <item>
#      <label>Autostart</label>
#      <binding>
#        <command>nasal</command>
#        <script>autochecklist.complete_checklists("startup");</script>
#      </binding>
#    </item>
#
#    For a no wait checklist execution (which will never fail), pass zero (0)
#    as the second argument:
#
#    autochecklist.complete_checklists(sequence:"startup", wait:0);
#
# 4. Optionally, configure automated execution using the following properties.
#    See comments within the autochecklist_init function for a description.
#
#   <checklists>
#     ...
#     <auto>
#       <completed-message>Checklists complete</completed-message>
#       <startup-message>Running checklists, please wait ...</startup-message>
#       <timeout-message>Some checks failed.</timeout-message>
#       <timeout-sec>10</timeout-sec>
#       <wait-sec>3</wait-sec>
#     <auto>
#   </checklists>
#
#   Note that messages are not displayed for no wait execution.
#
################################################################################

var checklists = nil;
var auto = nil;
var active = nil;
var expedited = nil;
var timeout_sec = nil;
var timeout_start = nil;
var wait_sec = nil;
var completed_message = nil;
var startup_message = nil;
var timeout_message = nil;

var autochecklist_init = func()
{
    # Root property tree path for checklists
    #
    checklists = props.globals.getNode("sim/checklists");

    # Root property tree path for auto checklists
    #
    auto = checklists.initNode("auto");

    # Flag to indicate that checklists are being completed automatically.
    # This can be used in Nasal bindings in the checklists to suppress a
    # binding when automated execution is in progress, e.g. for checklist
    # items that display dialogs.
    #
    active = auto.initNode("active", 0, "BOOL");

    # Flag to indicate that checklist execution is expedited, i.e. there
    # is no wait time between items. Typically indicates an in-air start. Note
    # that the expedited flag does not imply automated checklists are active.
    #
    expedited = auto.initNode("expedited", 0, "BOOL");

    # Timeout for completion of a checklist item. If the previous condition
    # is still not satisifed after this timeout, the checklist fails.
    #
    timeout_sec = auto.initNode("timeout-sec", 10, "INT");
    timeout_start = auto.initNode("timeout-start", 0.0, "DOUBLE");

    # If the previous checklist item is not complete, the process waits
    # this number of seconds before trying again.
    #
    wait_sec = auto.initNode("wait-sec", 3, "INT");

    # Messages
    #
    completed_message = auto.initNode("completed-message",
        "Checklists complete."
    );

    startup_message = auto.initNode("startup-message",
        "Running checklists, please wait ..."
    );

    timeout_message = auto.initNode("timeout-message",
        "Some checks failed. Try completing checklist manually."
    );
}

################################################################################

# Announces a message to the pilot
#
# @param message: the message to display
#
var announce = func(message)
{
    setprop("sim/messages/copilot", message);
    logprint(3, message);
}

# Resets the timestamp used for checking timeouts
#
var reset_timeout = func()
{
    timeout_start.setValue(0.0);
}

# Return true if the timeout period has been exceeded
#
var timed_out = func()
{
    var elapsed = getprop("sim/time/elapsed-sec") - timeout_start.getValue();
    return timeout_start.getValue() and elapsed > timeout_sec.getValue();
}

# Waits for the completion of an item, setting a timestamp for timeouts
#
# @param node: the node containing the list of checklist indexes to run
# @param from: the checklist item node from which to start
#
var wait_for_completion = func(node, from)
{
    var t = maketimer(wait_sec.getValue(), func {
        complete(node, 1, from);
    });
    t.singleShot = 1;
    t.start();

    if (!timeout_start.getValue()) {
        timeout_start.setValue(getprop("sim/time/elapsed-sec"));
    }
}

# Automatically complete a set of checklists defined by a series of indexes
# listed under the node argument. Not intended to be called from outside this
# script.
#
# @param node: the node containing the list of checklist indexes to run
# @param wait: whether to wait for the preceding binding to complete
# @param from: the checklist item node from which to start, default nil
#
var complete = func(node, wait, from = nil)
{
    var previous_condition = nil;
    var skipping = from != nil;

    foreach (var index; node.getChildren("index")) {
        var checklist = checklists.getChild("checklist", index.getValue());
        foreach (var item; checklist.getChildren("item")) {
            var condition = item.getNode("condition");
            if (skipping) {
                if (!item.equals(from)) {
                    previous_condition = condition;
                    continue;
                }
                skipping = 0;
            }
            if (wait) {
                if (props.condition(previous_condition)) {
                    reset_timeout();
                } else {
                    if (timed_out()) {
                        var title = checklist.getNode("title").getValue();
                        announce(title~": "~timeout_message.getValue());
                    } else {
                        wait_for_completion(node: node, from: item);
                    }
                    return;
                }
            }
            if (!props.condition(condition)) {
                foreach (var binding; item.getChildren("binding")) {
                    active.setValue(1);
                    props.runBinding(binding);
                    active.setValue(0);
                }
            }
            previous_condition = condition;
        }
    }
    if (wait) {
        announce(completed_message.getValue());
    }
}

################################################################################

# Complete a checklist sequence, typically called from a Nasal binding in a
# menu, e.g. Autostart, but could be assigned to a controller button or even
# called from a listener.
#
# @param sequence: the name of the checklist sequence to run
# @param wait: whether to wait for the preceding binding, default 1 (true)
#
var complete_checklists = func(sequence, wait = 1)
{
    var node = checklists.getNode(sequence);
    if (node != nil) {
        if (wait) {
            announce(startup_message.getValue());
        }
        expedited.setValue(!wait);
        complete(node, wait);
    } else {
        announce("Could not find checklist sequence called '"~sequence~"'");
    }
}

setlistener("/sim/signals/fdm-initialized", func() {
    autochecklist_init();
});