#
# EICAS message system
# initial version by jsb 05/2018
#

# simple pager to get a sub vector of messages
var Pager = {
    new: func(page_length, prop_path) {
        var obj = {
            parents: [me],
            page_length: 1,
            lengthN: props.getNode(prop_path~"/page_length",1),
            current_page: 1,
            pageN: props.getNode(prop_path~"/page",1),
            pagesN: props.getNode(prop_path~"/pages",1),
            last_result: 0,
            prop_path: prop_path,
            line_count: 0,
            pg_changed: 0,
        };
        obj.setPageLength(page_length);
        obj.setPage(1);
        setlistener(obj.pageN.getPath(), func(n) {
            obj.current_page = n.getValue();
            obj.pg_changed = 1;
        }, 0, 0);
        setlistener(obj.lengthN.getPath(), func(n) {
            obj.page_length = n.getValue();
            obj.pg_changed = 1;
        }, 0, 0);
        return obj;
    },

    pageChanged: func() {
        var c = me.pg_changed;
        me.pg_changed = 0;
        return c;
    },

    setPageLength: func(n) {
        me.page_length = int(n) or 1;
        me.lengthN.setIntValue(me.page_length);
        return me;
    },

    setPage: func(p) {
        me.current_page = int(p) or 1;
        me.pageN.setIntValue(me.current_page);
        return me;
    },

    getPageCount: func() {
        return me.page_count;
    },

    getCurrentPage: func() {
        return me.current_page;
    },

    # lines: vector of all messages
    # returns lines of current page; sticky lines will not be paged
    page: func(lines) {
        me.line_count = size(lines);
        #count sticky lines, assume sorted list
        for (var sticky = 0; sticky < me.line_count; sticky += 1) {
            if (lines[sticky].paging) break;
        }
        if (sticky >= me.page_length) {
            sticky = me.page_length;
            me.page_count = 1;
        }
        else {
            var page_lines = me.line_count - sticky;
            var len = me.page_length - sticky;
            me.page_count = int((page_lines - 1) / len) + 1;
        }
        me.pagesN.setValue(me.page_count);
        me.current_page = me.pageN.getValue() or 1;
        var start = len * (me.current_page-1) + sticky;
        # default to first page if page is invalid
        if (me.current_page > me.page_count) {
            me.setPage(1);
            start = sticky;
        }
        var end = start + len - 1;
        if (end >= me.line_count)
            end = me.line_count-1;
        #print("page l:"~me.line_count~" start "~start~" end "~end);
        var result = sticky ? lines[0:(sticky-1)] : [];
        if (start <= end) {
            return result~lines[start:end];
        }
        else return result;
    },
};

var MessageClass = {
    #static, increased by new()
    prio: 0,

    new: func(name, paging, prio=0) {
        var obj = {
            parents: [me],
            name: name,
            paging: paging,
            disabled: 0,
            color: [1,1,1],
        };
        if (prio)
            obj.prio = prio;
        else {
            obj.prio = me.prio;
            me.prio += 1;
        }
        return obj;
    },

    setColor: func(color) {
        if (color != nil)
            me.color = color;
        return me;
    },

    setPrio: func(prio) {
        me.prio = int(prio);
        return me;
    },

    enable: func { me.disabled = 0; },
    disable: func(bool = 1) { me.disabled = bool; },
    isDisabled: func { return me.disabled; },
};

var Message = {
    msg: "",
    prop: "",
    aural: "",
    condition: {
        eq: "equals",
        ne: "not equals",
        lt: "less than",
        gt: "greater than",
    },
};

var MessageSystem = {
    PAGING: 1,
    NO_PAGING: 0,

    new: func(page_length, prop_path) {
        if (!isint(page_length) or page_length < 1 or page_length > 100) {
            logprint(LOG_ALERT, "MessageSystem.new(): page_length must be an integer value between 1 and 100");
            return;
        }

        var obj = {
            parents: [me],
            rootN : props.getNode(prop_path,1),
            page_length: page_length,
            pager: Pager.new(page_length, prop_path),
            classes: [],
            messages: [],       # vector of vector of messages
            sounds: {},
            sound_path: "",
            sound_queue: "efis",
            active_messages: [],    # lists of active message IDs per class
            active_aurals: {},      # list of active aural warnings ((un-)set if corresponding message is (in-)active)
            msg_list: [],           # active message list (flat, sorted by class)
            first_changed_line: 0,  # for later optimisation: first changed line in msg_list
            changed: 1,
            powerN: nil,
            canvas_group: nil,
            page_indicator: nil,
            page_indicator_format: "Page %2d/%2d",
            lines: [],
        };
        return obj;
    },

    # setRootNode: func(n) {
        # me.rootN = n;
    # },

    # set power prop and add listener to start/stop all registered update functions
    setPowerProp: func(p) {
        me.powerN = props.getNode(p,1);
        setlistener(me.powerN, func(n) {
            if (n.getValue()) {
                me.init();
            }
        }, 1, 0);
    },

    # class_name:  identifier for msg class
    # paging:     true = normal paging, false = msg class is sticky at top of list
    # returns class id (int)
    addMessageClass: func(class_name, paging, color = nil) {
        var class = size(me.classes);
        me["new-msg"~class] = me.rootN.getNode("new-msg-"~class_name,1);
        me["new-msg"~class].setIntValue(0);
        append(me.classes, MessageClass.new(class_name, paging).setColor(color));
        append(me.active_messages, []);
        return class;
    },

    # addMessages creates a new msg class and add messages to it
    # class:     class id returned by addMessageClass();
    # messages:  vector of message objects (hashes)
    addMessages: func(class, messages) {
        forindex (var i; messages) {
            messages[i]["_class"] = class;
        }
        append(me.messages, messages);

        var simpleL = func(i){
            return func(n) {
                var val = n.getValue() or 0;
                me.setMessage(class, i, val);
            }
        };
        var eqL = func(i) {
            return func(n) {
                var val = n.getValue() or 0;
                if (val == messages[i].condition["eq"])
                    me.setMessage(class, i, 1);
                else me.setMessage(class, i, 0);
            }
        };
        var neL = func(i) {
            return func(n) {
                var val = n.getValue() or 0;
                if (val != messages[i].condition["ne"])
                    me.setMessage(class, i, 1);
                else me.setMessage(class, i, 0);
            }
        };
        var ltL = func(i) {
            return func(n) {
                var val = num(n.getValue()) or 0;
                if (val  < messages[i].condition["lt"])
                    me.setMessage(class, i, 1);
                else me.setMessage(class, i, 0);
            }
        };
        var gtL = func(i) {
            return func(n) {
                var val = num(n.getValue()) or 0;
                if (val > messages[i].condition["gt"])
                    me.setMessage(class, i, 1);
                else me.setMessage(class, i, 0);
            }
        };
        forindex (var i; messages) {
            if (messages[i].prop) {
                #print("addMessage "~i~" t:"~messages[i].msg~" p:"~messages[i].prop);
                var prop = props.getNode(messages[i].prop,1);
                # listeners won't work on aliases so find real node
                while (prop.getAttribute("alias")) {
                    prop = prop.getAliasTarget();
                }
                if (messages[i]["condition"] != nil) {
                    var c = messages[i]["condition"];
                    if (c["eq"] != nil) setlistener(prop, eqL(i), 1, 0);
                    if (c["ne"] != nil) setlistener(prop, eqL(i), 1, 0);
                    if (c["lt"] != nil) setlistener(prop, ltL(i), 1, 0);
                    if (c["gt"] != nil) setlistener(prop, gtL(i), 1, 0);
                }
                else setlistener(prop, simpleL(i), 1, 0);
            }
        }
    },

    setSoundPath: func(path) {
        me.sound_path = path;
    },

    addAuralAlert: func(id, filename, volume=1.0, path=nil) {
        if (typeof(id) == "hash") {
            logprint(DEV_ALERT, "First argument to addAuralAlert() must be a string but is a hash. Use addAuralAlerts() to pass all alerts in one hash.");
        }
        me.sounds[id] = {
            path: (path == nil) ? me.sound_path : path,
            file: filename,
            volume: volume,
            queue: me.sound_queue,
        };
        me.active_aurals[id] = 0;
    },

    addAuralAlerts: func(alert_hash) {
        if (typeof(alert_hash) != "hash") {
            logprint(DEV_ALERT, "MessageSystem.addAuralAlerts: parameter must be a hash!");
            return;
        }
        me.sounds = alert_hash;
        foreach (var k; keys(alert_hash)){
            me.active_aurals[k] = 0;
            if (typeof(me.sounds[k]) == "scalar") {
                me.sounds[k] = {file: me.sounds[k]};
            }
            me.sounds[k]["queue"] = me.sound_queue;
            if (me.sounds[k]["path"] == nil) {
                me.sounds[k]["path"] = me.sound_path;
            }
            if (me.sounds[k]["volume"] == nil) {
                me.sounds[k]["volume"] = 1.0;
            }
        }
    },

    # check per class queues and create list of all active messages not inhibited
    _updateList: func() {
        me.msg_list = [];
        forindex (var class; me.active_messages) {
            foreach (var id; me.active_messages[class]) {
                if (!me.classes[class].disabled and !me.messages[class][id]["disabled"])
                    append(me.msg_list, {
                        text: me.messages[class][id].msg,
                        color: me.classes[class].color,
                        paging: me.classes[class].paging });
            }
        }
    },

    _remove: func(class, msg) {
        var tmp = [];
        for (var i = 0; i < size(me.active_messages[class]); i += 1) {
            if (me.active_messages[class][i] != msg) {
                append(tmp, me.active_messages[class][i]);
            }
        }
        return tmp;
    },

    _isActive: func(class, msg) {
        foreach (var m; me.active_messages[class]) {
            if (m == msg) {
                return 1;
            }
        }
        return 0;
    },

    # (de-)activate message
    setMessage: func(class, msg_id, visible=1) {
        if (class >= size(me.classes))
            return;
        var isActive = me._isActive(class, msg_id);
        if ((isActive and visible) or (!isActive and !visible)) {
            # no change
            return;
        }
        if (!me.changed) {
            me.first_changed_line = me.pager.page_length;
        }
        #add message at head of list, 2DO: priority handling?!
        var aural = me.messages[class][msg_id]["aural"];
        if (visible) {
            me.active_messages[class] = [msg_id]~me.active_messages[class];
            # set new-msg flag in prop tree, e.g. to trigger sounds;
            # may be reset from outside this class so we can trigger again here
            me["new-msg"~class].setIntValue(1);
            if (aural != nil) {
                me.active_aurals[aural] = 1;
                me.auralAlert(aural);
            }
        }
        else {
            me.active_messages[class] = me._remove(class, msg_id);
            if (aural != nil) me.active_aurals[aural] = 0;
            # clear new-msg flag if last message is gone
            if (size(me.active_messages[class]) == 0)
                me["new-msg"~class].setIntValue(-1);
        }

        # count lines of classes with higher priority (= lower class id)
        # we do not need to update them as they did not change
        var unchanged = 0;
        for (var c = 0; c < class; c += 1) {
            unchanged += size(me.active_messages[c]);
        }
        if (me.first_changed_line > unchanged) {
            me.first_changed_line = unchanged;
        }
        #print("set c:"~class~" m:"~msg_id~" v:"~visible~ " 1upd:"~me.first_changed_line);
        # me._updateList();
        me.changed = 1;
    },

    auralAlert: func(aural) {
        if (me.sounds != nil and aural != nil) {
            if (me.active_aurals[aural])
                fgcommand("play-audio-sample", props.Node.new(me.sounds[aural]));
        }
    },

    #-- check for active messages and set new-msg flags.
    #   can be used on power up to trigger new-msg events.
    init: func {
        forindex (var class; me.active_messages) {
            if (size(me.active_messages[class])) {
                me["new-msg"~class].setIntValue(1);
            }
        }
        me.changed = 1;
        #hack for aural alerts
        #print("Enabling EICAS Message System sounds: /sim/sound/"~me.sound_queue~"/enabled = 1");
        setprop("/sim/sound/"~me.sound_queue~"/enabled", 1);
    },

    hasUpdate: func {
        return me.changed;
    },

    setPageLength: func(p) {
        if (p > size(me.lines))
            return;
        for (var i = math.min(p, me.page_length); i < size(me.lines); i += 1) {
            me.lines[i].setText("");
        }
        me.page_length = p;
        me.pager.setPageLength(p);
        me.updateCanvas();
        return me;
    },

    getPageLength: func {
        return me.page_length;
    },

    getFirstUpdateIndex: func {
        return me.first_changed_line;
    },

    # returns message queue and clears the hasUpdate flag
    getActiveMessages: func {
        if (me.changed) {
            me._updateList();
        }
        me.changed = 0;
        return me.msg_list;
    },

    #find message text, return id
    getMessageID: func(class, msgtext) {
        forindex (var id; me.messages[class]) {
            if (me.messages[class][id].msg == msgtext)
                return id;
        }
        return -1;
    },

    # inhibit message id (or all messages in class if no id is given)
    disableMessage: func(class, id = nil) {
        if (id != nil)
            me.messages[class][id]["disabled"] = 1;
        else forindex (var i; me.messages[class])
            me.messages[class][i]["disabled"] = 1;
    },

    # re-enable message id (or all messages in class if no id is given)
    enableMessage: func(class, id = nil) {
        if (id != nil)
            me.messages[class][id]["disabled"] = 0;
        else forindex (var i; me.messages[class])
            me.messages[class][i]["disabled"] = 0;
    },

    #
    #-- following methods are for message output on a canvas --
    #

    # pass an existing canvas group to create text elements on
    setCanvasGroup: func(group) {
        if (isa(group, canvas.Group)) {
            me.canvas_group = group;
        }
        else {
            me.canvas_group = nil;
            logprint(DEV_ALERT, "setCanvasGroup: argument is not a canvas group");
        }
        return me;
    },

    # create text elements for message lines in canvas group; call setCanvasGroup() first!
    createCanvasTextLines: func(left, top, line_spacing, font_size) {
        if (me.canvas_group == nil) return;
        me.lines = me.canvas_group.createChildren("text", me.page_length);
        forindex (var i; me.lines) {
            var l = me.lines[i];
            l.setAlignment("left-top").setTranslation(left, top + i*line_spacing);
            l.setFont("LiberationFonts/LiberationSans-Regular.ttf");
            l.setFontSize(font_size);
        }
        return me.lines;
    },

    # create text element for "page i of N"; call setCanvasGroup() first!
    # returns the text element
    createPageIndicator: func(left, top, font_size, format_string = nil) {
        if (me.canvas_group == nil) return;
        me.page_indicator = me.canvas_group.createChild("text");
        me.page_indicator.setAlignment("left-top").setTranslation(left, top);
        me.page_indicator.setFont("LiberationFonts/LiberationSans-Regular.ttf");
        me.page_indicator.setFontSize(font_size);
        if (format_string != nil)
            me.page_indicator_format = format_string;
        return me.page_indicator;
    },

    # call this regularly to update text lines on canvas
    updateCanvas: func() {
        # check if page change was selected
        var pg_changed = me.pager.pageChanged();
        if (!(pg_changed or me.changed))
            return 0;
        if (pg_changed) {
            # update all lines on screen
            me.first_changed_line = 0;
        }
        me._updateList();
        me.changed = 0;
        var messages = me.pager.page(me.msg_list);
        for (var i = me.first_changed_line; i < size(messages); i += 1) {
            me.lines[i].setText(messages[i].text);
            if (messages[i].color != nil)
                me.lines[i].setColor(messages[i].color);
        }
        # clear text from unused lines
        for (i; i < me.page_length; i += 1) {
            me.lines[i].setText("");
        }
        if (me.page_indicator != nil) {
            if (me.pager.getPageCount() > 1) {
                me.page_indicator.show();
                me.updatePageIndicator(me.pager.getCurrentPage(), me.pager.getPageCount());
            }
            else me.page_indicator.hide();
        }
    },

    updatePageIndicator: func(current, total) {
        me.page_indicator.setText(sprintf(me.page_indicator_format, current, total));
        return me;
    },
};