/*****************************************************************************
 *
 * NagVisObject.js - This class handles the visualisation of statefull objects
 *
 * Copyright (c) 2004-2016 NagVis Project (Contact: info@nagvis.org)
 *
 * License:
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *****************************************************************************/

/**
 * @author	Lars Michelsen <lm@larsmichelsen.com>
 */

const NagVisStatefulObject = NagVisObject.extend({
    has_state: true,

    // Stores the information from last refresh (Needed for change detection)
    last_state: null,
    // Array of member objects like services for hosts etc.
    members: [],
    // Timestamps for last event handling events if repeated events are enabled
    event_time_first: null,
    event_time_last: null,

    constructor: function (oConf) {
        this.base(oConf);
    },

    getMembers: function () {
        // Clear member array on every launch
        this.members = [];

        if (this.conf && this.conf.members && this.conf.members.length > 0) {
            for (let i = 0, len = this.conf.members.length; i < len; i++) {
                let oMember = this.conf.members[i];
                let oObj;

                switch (oMember.type) {
                    case "host":
                        oObj = new NagVisHost(oMember);
                        break;
                    case "service":
                        oObj = new NagVisService(oMember);
                        break;
                    case "hostgroup":
                        oObj = new NagVisHostgroup(oMember);
                        break;
                    case "servicegroup":
                        oObj = new NagVisServicegroup(oMember);
                        break;
                    case "dyngroup":
                        oObj = new NagVisDynGroup(oMember);
                        break;
                    case "aggr":
                        oObj = new NagVisAggr(oMember);
                        break;
                    case "map":
                        oObj = new NagVisMap(oMember);
                        break;
                    case "textbox":
                        oObj = new NagVisTextbox(oMember);
                        break;
                    case "container":
                        oObj = new NagVisContainer(oMember);
                        break;
                    case "shape":
                        oObj = new NagVisShape(oMember);
                        break;
                    case "line":
                        oObj = new NagVisLine(oMember);
                        break;
                    default:
                        alert("Error: Unknown member object type (" + oMember.type + ")");
                        break;
                }

                if (oObj !== null) {
                    this.members.push(oObj);
                }

                oObj = null;
                oMember = null;
            }
        }
    },

    getStatefulMembers: function () {
        const stateful = [];
        for (let i = 0, len = this.members.length; i < len; i++) {
            if (this.members[i].has_state) {
                stateful.push(this.members[i]);
            }
        }
        return stateful;
    },

    // Fetches member details lazily from the server when first needed (hover),
    // then calls callback. _members_pending_cbs being non-null signals a request
    // is in flight; all callers (including the first) are queued and fired together
    // once the response arrives.
    fetchMembers: function (callback) {
        if (this.conf.members && this.conf.members.length > 0) {
            this.getMembers();
            callback();
            return;
        }
        if (!this.conf.num_members || this.conf.num_members == 0) {
            callback();
            return;
        }
        if (!this._members_pending_cbs) this._members_pending_cbs = [];
        this._members_pending_cbs.push(callback);
        if (this._members_pending_cbs.length > 1) return; // request already in flight

        const flushPending = function () {
            const pending = this._members_pending_cbs;
            this._members_pending_cbs = null;
            for (let i = 0; i < pending.length; i++) pending[i]();
        }.bind(this);

        call_ajax(
            oGeneralProperties.path_server +
                "?mod=Map&act=getObjectMembers&show=" +
                g_view.id +
                "&i=" +
                this.conf.object_id,
            {
                response_handler: function (data) {
                    this.conf.members = data || [];
                    this.getMembers();
                    flushPending();
                }.bind(this),
                error_handler: flushPending
            }
        );
    },

    /**
     * PUBLIC saveLastState()
     *
     * Saves the current state in last state array for later change detection
     *
     * @author	Lars Michelsen <lm@larsmichelsen.com>
     */
    saveLastState: function () {
        this.last_state = {
            summary_state: this.conf.summary_state,
            summary_in_downtime: this.conf.summary_in_downtime,
            summary_stale: this.conf.summary_stale,
            summary_problem_has_been_acknowledged: this.conf.summary_problem_has_been_acknowledged,
            output: this.conf.output,
            perfdata: this.conf.perfdata
        };
    },

    /**
     * PUBLIC stateChanged()
     *
     * Check if a state change occured since last refresh
     *
     * @author	Lars Michelsen <lm@larsmichelsen.com>
     */
    stateChanged: function () {
        if (
            this.conf.summary_state != this.last_state.summary_state ||
            this.conf.summary_problem_has_been_acknowledged != this.last_state.summary_problem_has_been_acknowledged ||
            this.conf.summary_stale != this.last_state.summary_stale ||
            this.conf.summary_in_downtime != this.last_state.summary_in_downtime
        ) {
            return true;
        } else {
            return false;
        }
    },

    /**
     * PUBLIC stateChangedToWorse()
     *
     * Check if a state change occured to a worse state
     *
     * @author	Lars Michelsen <lm@larsmichelsen.com>
     */
    stateChangedToWorse: function () {
        let lastSubState = "normal";
        if (
            this.last_state.summary_problem_has_been_acknowledged &&
            this.last_state.summary_problem_has_been_acknowledged == 1
        ) {
            lastSubState = "ack";
        } else if (this.last_state.summary_in_downtime && this.last_state.summary_in_downtime == 1) {
            lastSubState = "downtime";
        } else if (this.last_state.summary_stale) {
            lastSubState = "stale";
        }

        // If there is no "last state" return true here
        if (!this.last_state.summary_state) {
            return true;
        }

        const lastWeight = oStates[this.last_state.summary_state][lastSubState];

        let subState = "normal";
        if (this.conf.summary_problem_has_been_acknowledged && this.conf.summary_problem_has_been_acknowledged == 1) {
            subState = "ack";
        } else if (this.conf.summary_in_downtime && this.conf.summary_in_downtime == 1) {
            subState = "downtime";
        } else if (this.conf.summary_stale) {
            subState = "stale";
        }

        const weight = oStates[this.conf.summary_state][subState];

        return lastWeight < weight;
    },

    /**
     * Returns true if the object is in non acked problem state
     */
    hasProblematicState: function () {
        // In case of acked/downtimed states this is no problematic state
        if (this.conf.summary_problem_has_been_acknowledged && this.conf.summary_problem_has_been_acknowledged == 1) {
            return false;
        } else if (this.conf.summary_in_downtime && this.conf.summary_in_downtime == 1) {
            return false;
        } else if (this.conf.summary_stale && this.conf.summary_stale) {
            return false;
        }

        const weight = oStates[this.conf.summary_state]["normal"];
        return weight > oStates["UP"]["normal"];
    },

    /**
     * PUBLIC outputChanged()
     *
     * Check if an output/perfdata change occured since last refresh
     *
     * @author	Lars Michelsen <lm@larsmichelsen.com>
     */
    outputOrPerfdataChanged: function () {
        return this.conf.output != this.last_state.output || this.conf.perfdata != this.last_state.perfdata;
    },

    // Updates the logical elements of the object
    update: function () {
        this.getMembers();
        this.replaceMacros();

        if (g_view.type === "map") {
            switch (this.conf.view_type) {
                case "line":
                    new ElementLine(this).addTo(this);
                    break;
                case "gadget":
                    new ElementGadget(this).addTo(this);
                    break;
                default:
                    new ElementIcon(this).addTo(this);
                    break;
            }
        }

        this.base();
    },

    updateAttrs: function (attrs, only_state) {
        // Save old state for later "change detection"
        this.saveLastState();

        this.base(attrs, only_state);
        this.updateMemberAttrs(attrs, only_state);

        if (!only_state) {
            this.replaceMacros();
        }

        // When the config has not changed, but the state, rerender the whole object
        // the config update is handled within NagVisObject()
        if (only_state && this.stateChanged()) this.render(); // erase, rerender and draw object

        if (this.stateChanged()) {
            /**
             * Additional eventhandling
             *
             * event_log=1/0
             * event_highlight=1/0
             * event_scroll=1/0
             * event_sound=1/0
             */

            // Only do eventhandling when object state changed to a worse state
            if (this.stateChangedToWorse()) {
                this.raiseEvents(true);
                this.initRepeatedEvents();
            }
            return true;
        } else {
            return false;
        }
    },

    transformAttributes: function () {
        this.replaceMacros();
    },

    updateMemberAttrs: function (attrs, only_state) {
        if (!this.members || !attrs.members) return;

        // Update already existing objects
        for (let i = 0, len = attrs.members.length; i < len; i++) {
            const member_attrs = attrs.members[i],
                updated = false;

            for (let a = 0, len2 = this.members.length; a < len2; a++) {
                const member = this.members[a];
                if (member_attrs.object_id == member.conf.object_id) {
                    member.updateAttrs(member_attrs, only_state);
                    break;
                }
            }
        }

        this.getMembers();
    },

    /**
     * Raise enabled frontend events for the object with the given object id
     */
    raiseEvents: function (stateChanged) {
        // - Highlight (Flashing)
        if (oPageProperties.event_highlight === "1") {
            if (this.conf.view_type && this.conf.view_type === "icon") {
                // Detach the handler
                setTimeout(
                    (function (obj_id) {
                        return function () {
                            flashIcon(
                                obj_id,
                                oPageProperties.event_highlight_duration,
                                oPageProperties.event_highlight_interval
                            );
                        };
                    })(this.conf.object_id),
                    0
                );
            } else {
                // FIXME: Atm only flash icons, not lines or gadgets
            }
        }

        // - Scroll to object
        if (oPageProperties.event_scroll === "1") {
            setTimeout(
                (function (x, y) {
                    return function () {
                        scrollSlow(x, y, 1);
                    };
                })(this.parsedX(), this.parsedY()),
                0
            );
        }

        // - Eventlog
        if (this.conf.type == "service") {
            if (stateChanged) {
                eventlog(
                    "state-change",
                    "info",
                    this.conf.type +
                        " " +
                        this.conf.name +
                        " " +
                        this.conf.service_description +
                        ": Old: " +
                        this.last_state.summary_state +
                        "/" +
                        this.last_state.summary_problem_has_been_acknowledged +
                        "/" +
                        this.last_state.summary_in_downtime +
                        " New: " +
                        this.conf.summary_state +
                        "/" +
                        this.conf.summary_problem_has_been_acknowledged +
                        "/" +
                        this.conf.summary_in_downtime
                );
            } else {
                eventlog(
                    "state-log",
                    "info",
                    this.conf.type +
                        " " +
                        this.conf.name +
                        " " +
                        this.conf.service_description +
                        ": State: " +
                        this.conf.summary_state +
                        "/" +
                        this.conf.summary_problem_has_been_acknowledged +
                        "/" +
                        this.conf.summary_in_downtime
                );
            }
        } else {
            if (stateChanged) {
                eventlog(
                    "state-change",
                    "info",
                    this.conf.type +
                        " " +
                        this.conf.name +
                        ": Old: " +
                        this.last_state.summary_state +
                        "/" +
                        this.last_state.summary_problem_has_been_acknowledged +
                        "/" +
                        this.last_state.summary_in_downtime +
                        " New: " +
                        this.conf.summary_state +
                        "/" +
                        this.conf.summary_problem_has_been_acknowledged +
                        "/" +
                        this.conf.summary_in_downtime
                );
            } else {
                eventlog(
                    "state-log",
                    "info",
                    this.conf.type +
                        " " +
                        this.conf.name +
                        ": State: " +
                        this.conf.summary_state +
                        "/" +
                        this.conf.summary_problem_has_been_acknowledged +
                        "/" +
                        this.conf.summary_in_downtime
                );
            }
        }

        // - Sound
        if (oPageProperties.event_sound === "1") {
            setTimeout(
                (function (obj_id) {
                    return function () {
                        playSound(obj_id, 1);
                    };
                })(this.conf.object_id),
                0
            );
        }
    },

    // Initializes repeated events (if configured to do so) after first event handling
    initRepeatedEvents: function () {
        // Are the events configured to be re-raised?
        if (isset(oViewProperties.event_repeat_interval) && oViewProperties.event_repeat_interval != 0) {
            this.event_time_first = iNow;
            this.event_time_last = iNow;
        }
    },

    /**
     * Checks wether or not repeated events need to be re-raised and re-raises
     * them if the time has come
     */
    checkRepeatEvents: function () {
        // Terminate repeated events after the state has changed to OK state
        if (!this.hasProblematicState()) {
            // Terminate, reset vars
            this.event_time_first = null;
            this.event_time_last = null;
            return;
        }

        // Terminate repeated events after duration has been reached when
        // a limited duration has been configured
        if (
            oViewProperties.event_repeat_duration != -1 &&
            this.event_time_first + oViewProperties.event_repeat_duration < iNow
        ) {
            // Terminate, reset vars
            this.event_time_first = null;
            this.event_time_last = null;
            return;
        }

        // Time for next event interval?
        if (this.event_time_last + oViewProperties.event_repeat_interval >= iNow) {
            this.raiseEvents(false);
            this.event_time_last = iNow;
        }
    },

    /**
     * Replaces macros of urls and hover_urls
     *
     * @author 	Lars Michelsen <lm@larsmichelsen.com>
     */
    replaceMacros: function () {
        let name = "";
        if (this.conf.type == "service") {
            name = "host_name";
        } else {
            name = this.conf.type + "_name";
        }

        if (this.conf.url && this.conf.url !== "") {
            if (this.conf.htmlcgi && this.conf.htmlcgi !== "") {
                this.conf.url = this.conf.url.replace(getRegEx("htmlcgi", "\\[htmlcgi\\]", "g"), this.conf.htmlcgi);
            } else {
                this.conf.url = this.conf.url.replace(
                    getRegEx("htmlcgi", "\\[htmlcgi\\]", "g"),
                    oGeneralProperties.path_cgi
                );
            }

            this.conf.url = this.conf.url.replace(
                getRegEx("htmlbase", "\\[htmlbase\\]", "g"),
                oGeneralProperties.path_base
            );

            this.conf.url = this.conf.url.replace(
                getRegEx(name, "\\[" + name + "\\]", "g"),
                encodeURIComponent(this.conf.name)
            );
            if (this.conf.type == "service") {
                this.conf.url = this.conf.url.replace(
                    getRegEx("service_description", "\\[service_description\\]", "g"),
                    encodeURIComponent(this.conf.service_description)
                );
            }

            if (this.conf.type != "map") {
                this.conf.url = this.conf.url.replace(
                    getRegEx("backend_id", "\\[backend_id\\]", "g"),
                    encodeURIComponent(this.conf.backend_id)
                );
            }
        }
    },

    highlight: function (show) {
        // FIXME: Highlight lines in the future too
        if (this.conf.view_type !== "icon") return;

        let oObjIcon = document.getElementById(this.conf.object_id + "-icon");
        let oObjIconDiv = document.getElementById(this.conf.object_id + "-icondiv");

        let sColor = oStates[this.conf.summary_state].color;

        // Use these classes (icon-flashing-<state>) in your custom css to customise flashing icons
        // e.g. icon-flashing-critical, icondiv-flashing-critical
        // Note you may need to use '!important' to override the inline default
        //
        let sFlashingClass = "icon-flashing-" + this.conf.summary_state.toLowerCase();
        let sFlashingDivClass = "icondiv-flashing-" + this.conf.summary_state.toLowerCase();

        this.bIsFlashing = show;
        if (show) {
            oObjIcon.style.outline = "5px solid " + sColor;
            oObjIcon.classList.add(sFlashingClass);
            oObjIconDiv.classList.add(sFlashingDivClass);
        } else {
            oObjIcon.style.outline = "none";
            oObjIcon.classList.remove(sFlashingClass);
            oObjIconDiv.classList.remove(sFlashingDivClass);
        }

        sColor = null;
        sFlashingClass = null;
        sFlashingDivClass = null;
        oObjIconDiv = null;
        oObjIcon = null;
    }
});
