/*****************************************************************************
 *
 * ElementLine.js - This class handles the visualisation of lines
 *
 * 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.
 *
 *****************************************************************************/

const ElementLineControls = Element.extend({
    render: function () {
        // don't render the controls during normal render calls. We
        // want to keep the number of DOM objects low, so only render
        // them when being unlocked for the first time. But when the
        // element has already been rendered, then re-render it!
        if (this.dom_obj) this._render();
    },

    draw: function () {
        // When locked: Don't draw on regular draw calls (only draw when locked/unlocked)
        if (!this.obj.bIsLocked) this.base();
    },

    unlock: function () {
        if (!this.dom_obj) this._render();
        this._draw();
    },

    lock: function () {
        this.erase();
    },

    place: function () {
        // FIXME: This should be possible without re-rendering everything
        if (!this.obj.bIsLocked) {
            this.erase();
            this._render();
            this.draw();
        }
    },

    //
    // END OF PUBLIC METHODS
    //

    _draw: function () {
        Element.prototype.draw.call(this);
    },

    _render: function () {
        const container = document.createElement("div");
        container.setAttribute("id", this.obj.conf.object_id + "-controls");
        this.dom_obj = container;

        const x = this.obj.parseCoords(this.obj.conf.x, "x");
        const y = this.obj.parseCoords(this.obj.conf.y, "y");
        const size = 10;

        for (let i = 0, l = x.length; i < l; i++) this.renderDragger(i, x[i], y[i], -size / 2, -size / 2, size);

        if (this.hasTwoParts())
            this.renderMidToggle(
                x.length + 2,
                this.obj.getLineMid(this.obj.conf.x, "x"),
                this.obj.getLineMid(this.obj.conf.y, "y"),
                20 - size / 2,
                -size / 2 + 5,
                size
            );
    },

    hasTwoParts: function () {
        return (
            this.obj.conf.view_type === "line" &&
            (this.obj.conf.line_type == 10 ||
                this.obj.conf.line_type == 13 ||
                this.obj.conf.line_type == 14 ||
                this.obj.conf.line_type == 15)
        );
    },

    renderDragger: function (num, objX, objY, offX, offY, size) {
        const ctl = document.createElement("div");
        this.dom_obj.appendChild(ctl);
        ctl.setAttribute("id", this.obj.conf.object_id + "-drag-" + num);
        ctl.className = "control drag";
        // FIXME: Multilanguage
        ctl.title = "Move object";
        ctl.style.zIndex = parseInt(this.obj.conf.z) + 1;
        ctl.style.width = addZoomFactor(size) + "px";
        ctl.style.height = addZoomFactor(size) + "px";
        ctl.style.left = objX + offX + "px";
        ctl.style.top = objY + offY + "px";
        ctl.objOffsetX = offX;
        ctl.objOffsetY = offY;

        const img = document.createElement("img");
        img.src = "../../frontend/nagvis-js/images/internal/control_drag.png";
        img.style.width = addZoomFactor(size) + "px";
        img.style.height = addZoomFactor(size) + "px";
        ctl.appendChild(img);

        makeDragable(ctl, this.obj, this.obj.saveObject, this.obj.moveObject);
    },

    // Adds the modify button to the controls including all eventhandlers
    renderMidToggle: function (num, objX, objY, offX, offY, size) {
        let ctl = document.createElement("div");
        this.dom_obj.appendChild(ctl);
        ctl.setAttribute("id", this.obj.conf.object_id + "-togglemid-" + num);
        ctl.className = "control togglemid";
        // FIXME: Multilanguage
        if (this.obj.bIsLocked) ctl.title = "Unlock line middle";
        else ctl.title = "Lock line middle";
        ctl.style.zIndex = parseInt(this.obj.conf.z) + 1;
        ctl.style.width = addZoomFactor(size) + "px";
        ctl.style.height = addZoomFactor(size) + "px";
        ctl.style.left = objX + offX + "px";
        ctl.style.top = objY + offY + "px";
        ctl.objOffsetX = offX;
        ctl.objOffsetY = offY;

        const img = document.createElement("img");
        if (this.isMidLocked()) img.src = "../../frontend/nagvis-js/images/internal/control_locked.png";
        else img.src = "../../frontend/nagvis-js/images/internal/control_unlocked.png";
        img.style.width = addZoomFactor(size) + "px";
        img.style.height = addZoomFactor(size) + "px";
        ctl.appendChild(img);

        addEvent(
            ctl,
            "click",
            (function (element_obj) {
                return function (event) {
                    event = event || window.event;
                    element_obj.toggleMidLock();
                    contextHide();
                    return preventDefaultEvents(event);
                };
            })(this)
        );
        ctl = null;
    },

    isMidLocked: function () {
        return this.obj.conf.x.split(",").length == 2;
    },

    /**
     * Toggles the position of the line middle. The mid of the line
     * can either be the 2nd of three line coords or is automaticaly
     * the middle between two line coords.
     */
    toggleMidLock: function () {
        // What is the current state?
        let x = this.obj.conf.x.split(",");
        let y = this.obj.conf.y.split(",");

        if (this.isMidLocked()) {
            // The line has 2 coords configured
            // - Calculate and add the 3rd coord as 2nd
            // - Add a drag control for the 2nd coord
            this.obj.conf.x = [
                x[0],
                middle(
                    this.obj.parseCoords(this.obj.conf.x, "x", false)[0],
                    this.obj.parseCoords(this.obj.conf.x, "x", false)[1],
                    this.obj.conf.line_cut
                ),
                x[1]
            ].join(",");
            this.obj.conf.y = [
                y[0],
                middle(
                    this.obj.parseCoords(this.obj.conf.y, "y", false)[0],
                    this.obj.parseCoords(this.obj.conf.y, "y", false)[1],
                    this.obj.conf.line_cut
                ),
                y[1]
            ].join(",");
        } else {
            // The line has 3 coords configured
            // - Remove the 2nd coord
            // - Remove the drag control for the 2nd coord
            this.obj.conf.x = [x[0], x[2]].join(",");
            this.obj.conf.y = [y[0], y[2]].join(",");
        }

        const parts = g_view.unproject(this.obj.conf.x.toString().split(","), this.obj.conf.y.toString().split(","));
        x = parts[0].join(",");
        y = parts[1].join(",");

        // send to server
        saveObjectAttr(this.obj.conf.object_id, { x: x, y: y });

        // redraw the whole object
        this.obj.render();
    }
});

const ElementLine = Element.extend({
    line_container: null,
    parts: null,
    canvas: null,
    perfdata: null,
    // The link area is some kind of a hack. The canvas detects whether or not
    // the mouse moves over the line. In the momennt the mouse moves over the
    // line, the link_area div node is moved at the mouse position to give the
    // user all the controls related to the line which is currently hovered.
    link_area: null,

    constructor: function (obj) {
        this.parts = [];
        this.base(obj);
    },

    update: function () {
        new ElementLineControls(this.obj).addTo(this.obj);
    },

    updateAttrs: function (only_state) {
        if (
            !only_state ||
            (this.isWeathermapLine() && (this.obj.stateChanged() || this.obj.outputOrPerfdataChanged()))
        ) {
            this.redrawLine();
        }
    },

    render: function () {
        if (this.isWeathermapLine()) this.parsePerfdata();

        const container = document.createElement("div");
        container.setAttribute("id", this.obj.conf.object_id + "-linediv");
        container.className = "line";
        this.dom_obj = container;

        // Create line div
        const oLineDiv = document.createElement("div");
        this.line_container = oLineDiv;
        container.appendChild(oLineDiv);
        oLineDiv.setAttribute("id", this.obj.conf.object_id + "-line");
        // the objects canvas might hide icons behind it, put it one layer down,
        // because normally icons and lines are on the same z-index
        oLineDiv.style.zIndex = parseInt(this.obj.conf.z) - 1;

        this.calcLineParts();
        this.renderLine();
        this.renderActionContainer();
        this.renderLinkArea();
        this.renderLabels();
    },

    place: function () {
        this.redrawLine();
    },

    unlock: function () {
        this.toggleActionContainer();
    },

    lock: function () {
        this.toggleActionContainer();
    },

    //
    // END OF PUBLIC METHODS
    //

    redrawLine: function () {
        // Totally redraw the line when moving the line anchors arround. But keep the trigger
        // object because it saves the attached event handlers
        const trigger_obj = this.obj.trigger_obj;
        trigger_obj.parentNode.removeChild(trigger_obj);
        this.clearActionContainer();

        this.erase();
        this.obj.trigger_obj = trigger_obj;
        this.render();
        this.draw();
    },

    renderActionContainer: function () {
        // This is only the container for the hover/label elements. The real area or labels
        // are added later. But this container gets all event handlers assigned. Because
        // the line is using erase(), render(), draw() within place() which is called while
        // the user moves the object, this dom node must not be re-created, because this
        // would remove all event handlers
        let oLink;
        if (!this.obj.trigger_obj) {
            oLink = document.createElement("a");
            oLink.setAttribute("id", this.obj.conf.object_id + "-linelink");
            oLink.className = "linelink";
            this.obj.trigger_obj = oLink;
        } else {
            oLink = this.obj.trigger_obj;
            this.clearActionContainer();
        }
        this.dom_obj.appendChild(oLink);
        if (this.obj.conf.url) {
            oLink.href = this.obj.conf.url;
            oLink.target = this.obj.conf.url_target;
        } else {
            oLink.href = "javascript:void(0)";
        }
    },

    clearActionContainer: function () {
        while (this.obj.trigger_obj.firstChild) this.obj.trigger_obj.removeChild(this.obj.trigger_obj.firstChild);
    },

    toggleActionContainer: function () {
        // Hide if not needed, show if needed
        if (!this.obj.needsLineHoverArea()) this.obj.trigger_obj.style.display = "none";
        else this.obj.trigger_obj.style.display = "block";
    },

    calcLineParts: function () {
        this.parts = [];

        const x = this.obj.parseCoords(this.obj.conf.x, "x");
        const y = this.obj.parseCoords(this.obj.conf.y, "y");

        // Convert all coords to int
        for (let i = 0; i < x.length; i++) {
            x[i] = parseInt(x[i], 10);
            y[i] = parseInt(y[i], 10);
        }

        let xStart = x[0];
        let yStart = y[0];
        let xEnd = x[x.length - 1];
        let yEnd = y[y.length - 1];

        const adjustedPoints = this.cutLineBeyondViewport(xStart, yStart, xEnd, yEnd);
        if (adjustedPoints === null) {
            return; // Line won't be visible -> this.parts left empty -> no rendering
        }

        xStart = adjustedPoints[0];
        yStart = adjustedPoints[1];
        xEnd = adjustedPoints[2];
        yEnd = adjustedPoints[3];

        let width = addZoomFactor(this.obj.conf.line_width);
        if (width <= 0) width = 1; // minimal width for lines

        // Lines meeting point position
        const cut = this.obj.conf.line_cut;

        switch (this.obj.conf.line_type) {
            case "11": // ---> lines
                this.renderArrow(0, xStart, yStart, xEnd, yEnd, width);
                break;
            case "12": // --- lines
                this.renderSimpleLine(0, xStart, yStart, xEnd, yEnd, width);
                break;
            case "10":
            case "13":
            case "14":
            case "15": {
                // two part lines
                let xMid, yMid;
                if (x.length == 2) {
                    xMid = middle(xStart, xEnd, cut);
                    yMid = middle(yStart, yEnd, cut);
                } else {
                    xMid = x[1];
                    yMid = y[1];
                }

                this.renderArrow(0, xStart, yStart, xMid, yMid, width);
                this.renderArrow(1, xEnd, yEnd, xMid, yMid, width);
                break;
            }
            default:
                alert("Error: Unknown line type");
        }
    },

    // Calculates the colors of a line part
    calcColors: function (id) {
        let color = "#FFCC66";
        const border_color = "#000000";

        // Get the fill color depending on the object state
        switch (this.obj.conf.summary_state) {
            case "UNREACHABLE":
            case "DOWN":
            case "CRITICAL":
            case "WARNING":
            case "UNKNOWN":
            case "ERROR":
            case "UP":
            case "OK":
            case "PENDING":
                color = oStates[this.obj.conf.summary_state].color;
                break;
        }

        // Adjust fill color based on perfdata for weathermap lines
        if (this.isWeathermapLine()) color = this.calcWeathermapColor(id);

        // ack/downtime/staleness lighten the color a bit
        if (
            this.obj.conf.summary_problem_has_been_acknowledged == 1 ||
            this.obj.conf.summary_in_downtime == 1 ||
            this.obj.conf.summary_stale
        ) {
            color = lightenColor(color, 100, 100, 100);

            if (
                this.obj.conf.summary_problem_has_been_acknowledged == 1 &&
                typeof oStates[this.obj.conf.summary_state].ack_bgcolor != "undefined" &&
                oStates[this.obj.conf.summary_state].ack_bgcolor != ""
            ) {
                color = oStates[this.obj.conf.summary_state].ack_bgcolor;
            }
        }

        return [color, border_color];
    },

    isWeathermapLine: function () {
        return this.obj.conf.line_type == 13 || this.obj.conf.line_type == 14 || this.obj.conf.line_type == 15;
    },

    renderLine: function () {
        if (this.parts.length === 0) {
            return;
        }

        let allX = [],
            allY = [];
        for (let i = 0, len = this.parts.length; i < len; i++) {
            allX = allX.concat(this.parts[i][2]);
            allY = allY.concat(this.parts[i][3]);
        }
        const xMin = min(allX);
        const yMin = min(allY);
        const xMax = max(allX);
        const yMax = max(allY);
        const border = 5;

        const canvas = document.createElement("canvas");
        this.canvas = canvas;

        if (!canvas.getContext) return; // FIXME: Show an error message?

        canvas.setAttribute("id", this.obj.conf.object_id + "-canvas");
        canvas.style.position = "absolute";
        this.line_container.appendChild(canvas);

        canvas.style.left = xMin - border + "px";
        canvas.style.top = yMin - border + "px";
        canvas.width = Math.round(xMax - xMin) + 2 * border;
        canvas.height = Math.round(yMax - yMin) + 2 * border;
        // the objects canvas might hide icons behind it, put it one layer down,
        // because normally icons and lines are on the same z-index
        canvas.style.zIndex = parseInt(this.obj.conf.z) - 1;

        addEvent(canvas, "mousemove", this.handleMouseMove.bind(this));

        const ctx = canvas.getContext("2d");
        if (!ctx) return; // silently skip

        // On high resolution devices like e.g. 4k screens where
        // the page is not rendered 1:1 but instead shown scaled,
        // the renderd lines look bad if they had been rendered 1:1.
        // Fix this by scaling the whole canvas.
        const devicePixelRatio = window.devicePixelRatio || 1;
        const backingStoreRatio =
            ctx.webkitBackingStorePixelRatio ||
            ctx.mozBackingStorePixelRatio ||
            ctx.msBackingStorePixelRatio ||
            ctx.oBackingStorePixelRatio ||
            ctx.backingStorePixelRatio ||
            1;
        const ratio = devicePixelRatio / backingStoreRatio;
        if (devicePixelRatio !== backingStoreRatio) {
            const oldWidth = canvas.width;
            const oldHeight = canvas.height;

            canvas.width = oldWidth * ratio;
            canvas.height = oldHeight * ratio;

            canvas.style.width = oldWidth + "px";
            canvas.style.height = oldHeight + "px";

            // now scale the context to counter
            // the fact that we've manually scaled
            // our canvas element
            ctx.scale(ratio, ratio);
        }

        // Now start rendering by first clearing the canvas.
        // Then start painting the single line parts
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        let part;
        for (let i = 0, len = this.parts.length; i < len; i++) {
            part = this.parts[i];

            ctx.fillStyle = part[4][0];

            ctx.beginPath();
            ctx.moveTo(part[2][0] - xMin + border, part[3][0] - yMin + border);

            for (let a = 1, len2 = part[2].length; a < len2; a++)
                ctx.lineTo(part[2][a] - xMin + border, part[3][a] - yMin + border);

            ctx.fill();

            // border
            ctx.lineWidth = 1;
            ctx.strokeStyle = part[4][1];
            ctx.stroke();
        }
    },

    // This function renders an arrow like it is used on NagVis maps
    // It renders following line types: --->
    renderArrow: function (id, x1, y1, x2, y2, w) {
        const xCoord = [
            x1 + newX(x2 - x1, y2 - y1, 0, w),
            x2 + newX(x2 - x1, y2 - y1, -4 * w, w),
            x2 + newX(x2 - x1, y2 - y1, -4 * w, 2 * w),
            x2,
            x2 + newX(x2 - x1, y2 - y1, -4 * w, -2 * w),
            x2 + newX(x2 - x1, y2 - y1, -4 * w, -w),
            x1 + newX(x2 - x1, y2 - y1, 0, -w),
            x1 + newX(x2 - x1, y2 - y1, 0, w)
        ];
        const yCoord = [
            y1 + newY(x2 - x1, y2 - y1, 0, w),
            y2 + newY(x2 - x1, y2 - y1, -4 * w, w),
            y2 + newY(x2 - x1, y2 - y1, -4 * w, 2 * w),
            y2,
            y2 + newY(x2 - x1, y2 - y1, -4 * w, -2 * w),
            y2 + newY(x2 - x1, y2 - y1, -4 * w, -w),
            y1 + newY(x2 - x1, y2 - y1, 0, -w),
            y1 + newY(x2 - x1, y2 - y1, 0, w)
        ];

        this.renderLinePart(id, [x1, y1], [x2, y2], xCoord, yCoord);
    },

    // This function renders simple lines (without arrow)
    renderSimpleLine: function (id, x1, y1, x2, y2, w) {
        const xCoord = [
            x1 + newX(x2 - x1, y2 - y1, 0, w),
            x2 + newX(x2 - x1, y2 - y1, w, w),
            x2 + newX(x2 - x1, y2 - y1, w, -w),
            x1 + newX(x2 - x1, y2 - y1, 0, -w),
            x1 + newX(x2 - x1, y2 - y1, 0, w)
        ];
        const yCoord = [
            y1 + newY(x2 - x1, y2 - y1, 0, w),
            y2 + newY(x2 - x1, y2 - y1, w, w),
            y2 + newY(x2 - x1, y2 - y1, w, -w),
            y1 + newY(x2 - x1, y2 - y1, 0, -w),
            y1 + newY(x2 - x1, y2 - y1, 0, w)
        ];

        this.renderLinePart(id, [x1, y1], [x2, y2], xCoord, yCoord);
    },

    renderLinePart: function (id, start, end, x, y) {
        this.parts.push([start, end, x, y, this.calcColors(id)]);
    },

    handleMouseMove: function (event) {
        event = event || window.event;

        if (getTargetRaw(event).tagName !== "CANVAS") return true;

        // document.body.scrollTop does not work in IE
        const scrollTop = document.body.scrollTop ? document.body.scrollTop : document.documentElement.scrollTop;
        const scrollLeft = document.body.scrollLeft ? document.body.scrollLeft : document.documentElement.scrollLeft;

        // Get the mouse position relative to window
        const x = event.clientX - getSidebarWidth() + scrollLeft;
        const y = event.clientY - getHeaderHeight() + scrollTop;

        // Check whether or not the given coord is within the line part coords
        const pnpoly = function (nvert, vertx, verty, testx, testy) {
            let i,
                j,
                c = false;
            for (i = 0, j = nvert - 1; i < nvert; j = i++) {
                if (
                    verty[i] > testy != verty[j] > testy &&
                    testx < ((vertx[j] - vertx[i]) * (testy - verty[i])) / (verty[j] - verty[i]) + vertx[i]
                ) {
                    c = !c;
                }
            }
            return c;
        };

        let part = null,
            over = false;
        for (let i = 0, len = this.parts.length; i < len; i++) {
            part = this.parts[i];
            if (pnpoly(part[2].length, part[2], part[3], x, y)) {
                over = true;
                break;
            }
        }

        if (over) {
            add_class(this.canvas, "active");
            // move the link area below the cursor to make actions possible
            this.link_area.style.display = "";
            this.link_area.style.left = x - 5 + "px";
            this.link_area.style.top = y - 5 + "px";

            if (usesSource("worldmap")) this.obj.marker._bringToFront();
        } else {
            remove_class(this.canvas, "active");
            this.link_area.style.display = "none";

            if (usesSource("worldmap")) this.obj.marker._resetZIndex();
        }
    },

    renderLabels: function () {
        if (!this.isWeathermapLine()) return; // Only weathermap lines have labels at the moment

        if (!this.obj.conf.line_label_show || this.obj.conf.line_label_show !== "1") return; // skip over when labels are disabled

        this.renderLabel(0);
        this.renderLabel(1);
    },

    getLabelWidth: function (str) {
        if (str && str.length > 0) return (str.length / 2) * 9;
        else return 10;
    },

    renderLabel: function (id) {
        if (this.parts.length === 0) return;
        if (this.perfdata === null) return;

        const y_offset = parseInt(this.obj.conf.line_label_y_offset);

        const x1 = this.parts[id][0][0],
            y1 = this.parts[id][0][1],
            x2 = this.parts[id][1][0],
            y2 = this.parts[id][1][1];

        const cut = id == 0 ? this.obj.conf.line_label_pos_in : this.obj.conf.line_label_pos_out;

        const x = middle(x1, x2, cut),
            y = middle(y1, y2, cut) + y_offset;

        let txt;
        if (this.obj.conf.line_type == 13 || this.obj.conf.line_type == 14) {
            // Show percentage label
            txt = this.perfdata[id][1] + this.perfdata[id][2];
        } else if (this.obj.conf.line_type == 15) {
            // Show only bandwidth label
            txt = this.perfdata[2 + id][1] + this.perfdata[2 + id][2];
        }

        // Maybe use function to detect the real height in future
        const labelHeight = 21;
        let labelWidth = this.getLabelWidth(txt);

        this.obj.trigger_obj.appendChild(
            renderNagVisTextbox(
                this.obj.conf.object_id + "-link" + id,
                "#ffffff",
                "#000000",
                x - labelWidth,
                parseInt(y - labelHeight / 2),
                this.obj.conf.z,
                "auto",
                "auto",
                "<b>" + txt + "</b>"
            )
        );

        // Paint the second label in case of line type 14
        if (this.obj.conf.line_type == 14) {
            txt = this.perfdata[2 + id][1] + this.perfdata[2 + id][2];
            labelWidth = this.getLabelWidth(txt);
            this.obj.trigger_obj.appendChild(
                renderNagVisTextbox(
                    this.obj.conf.object_id + "-link" + (id + 1),
                    "#ffffff",
                    "#000000",
                    x - labelWidth,
                    parseInt(y + labelHeight / 2),
                    this.obj.conf.z,
                    "auto",
                    "auto",
                    "<b>" + txt + "</b>"
                )
            );
        }
    },

    renderLinkArea: function () {
        const div = document.createElement("div");
        this.link_area = div;
        div.setAttribute("id", this.obj.conf.object_id + "-link");
        div.style.position = "absolute";
        div.style.top = "-100px"; // out of screen by default
        div.style.zIndex = parseInt(this.obj.conf.z) + 1;
        div.style.width = "10px";
        div.style.height = "10px";

        this.obj.trigger_obj.appendChild(div);
    },

    parsePerfdata: function () {
        this.perfdata = null;

        /*
         Convert perfdata string to structured array. Data returne by this
         function is an array of arrays containing these elements:
           0 = label
           1 = value
           2 = unit of measure (UOM)
           3 = warning
           4 = critical
           5 = minimum
           6 = maximum
        */
        let perf = this.parsePerfdataString();

        if (!perf) {
            this.addWeathermapLineError("Perfdata string is empty");
            return;
        }

        if (!isset(perf[0])) {
            this.addWeathermapLineError("Value 1 is empty");
            return;
        }

        if (!isset(perf[1])) {
            this.addWeathermapLineError("Value 2 is empty");
            return;
        }

        if (this.obj.conf.line_type == 14 || this.obj.conf.line_type == 15) {
            if (!isset(perf[2])) {
                this.addWeathermapLineError("Value 3 is empty");
                return;
            }

            if (!isset(perf[3])) {
                this.addWeathermapLineError("Value 4 is empty");
                return;
            }
        }

        // This is the correct place to handle other perfdata format than the percent value
        // When no UOM is set try to calculate something...
        // This can fix the perfdata values from Checkmks if and if64 checks.
        // The assumption is that there are perfdata values 'in' and 'out' with byte rates
        // and maximum values given to be able to calculate the percentage usage
        if (perf[0][2] === null || perf[0][2] === "" || perf[1][2] === null || perf[1][2] === "") {
            perf = this.calculateUsage(perf, this.obj.conf.output);
        }

        this.perfdata = perf;
    },

    addWeathermapLineError: function (e) {
        this.obj.conf.summary_output += " (Weathermap Line Error: " + e + ")";
    },

    calcWeathermapColor: function (id) {
        if (this.obj.conf.summary_state == "ERROR") return oStates["ERROR"].color;

        if (!this.perfdata || !this.perfdata[id]) return oStates["CRITICAL"].color;

        if (this.perfdata[id][2] == "%" && this.perfdata[id][1] !== null) {
            return this.getColorFill(this.perfdata[id][1]);
        } else {
            this.obj.conf.summary_output += " (Weathermap Line Error: Value " + id + " is not a percentage value)";
            return oStates["ERROR"].color;
        }
    },

    /**
     * This function returns the color to use for this line depending on the
     * given percentage usage and on the configured options for this object
     */
    getColorFill: function (perc) {
        let ranges = this.obj.conf.line_weather_colors.split(",");
        // 0 contains the percentage until this color is used
        // 1 contains the color to be used
        for (let i = 0; i < ranges.length; i++) {
            let parts = ranges[i].split(":");
            if (parseFloat(perc) <= parts[0]) return parts[1];
            parts = null;
        }
        ranges = null;
        return "#000000";
    },

    /**
     * Loops all perfdata sets and searches for labels "in" and "out"
     * with an empty UOM. If found it uses the current value and max value
     * for calculating the percentage usage and also the current usage.
     */
    calculateUsage: function (oldPerfdata, output) {
        const newPerfdata = [];
        let foundIn = false;
        let foundOut = false;
        let line_label_in = "in";
        let line_label_out = "out";

        // Checkmk if/if64 checks support switching between bytes/bits.
        let display_bits = false;
        const reBits =
            /\bIn(?:\s+average\s+[^:]+)?:\s*[0-9][0-9.,]*\s*\S*Bit\/s\b[\s\S]*?\bOut(?:\s+average\s+[^:]+)?:\s*[0-9][0-9.,]*\s*\S*Bit\/s\b/i;

        if (output.match(reBits)) {
            display_bits = true;
        }

        // This loop takes perfdata with the labels "in" and "out" and uses the current value
        // and maximum values to parse the percentage usage of the line
        if (typeof this.obj.conf.line_label_in !== "undefined") {
            line_label_in = this.obj.conf.line_label_in;
        }
        if (typeof this.obj.conf.line_label_out !== "undefined") {
            line_label_out = this.obj.conf.line_label_out;
        }
        for (let i = 0; i < oldPerfdata.length; i++) {
            if (oldPerfdata[i][0] == line_label_in && (oldPerfdata[i][2] === null || oldPerfdata[i][2] === "")) {
                newPerfdata[0] = this.perfdataCalcPerc(oldPerfdata[i]);
                if (!display_bits) {
                    newPerfdata[2] = this.perfdataCalcBytesReadable(oldPerfdata[i]);
                } else {
                    oldPerfdata[i][1] *= 8; // convert those hakish bytes to bits
                    newPerfdata[2] = this.perfdataCalcBitsReadable(oldPerfdata[i]);
                }
                foundIn = true;
            }
            if (oldPerfdata[i][0] == line_label_out && (oldPerfdata[i][2] === null || oldPerfdata[i][2] === "")) {
                newPerfdata[1] = this.perfdataCalcPerc(oldPerfdata[i]);
                if (!display_bits) {
                    newPerfdata[3] = this.perfdataCalcBytesReadable(oldPerfdata[i]);
                } else {
                    oldPerfdata[i][1] *= 8; // convert those hakish bytes to bits
                    newPerfdata[3] = this.perfdataCalcBitsReadable(oldPerfdata[i]);
                }
                foundOut = true;
            }
        }
        if (foundIn && foundOut) return newPerfdata;
        else return oldPerfdata;
    },

    /**
     * Transform bits in a perfdata set to a human readable value
     */
    perfdataCalcBitsReadable: function (set, kiloMultiplier = 1000) {
        const KB = kiloMultiplier;
        const MB = kiloMultiplier * kiloMultiplier;
        const GB = kiloMultiplier * kiloMultiplier * kiloMultiplier;
        if (set[1] > GB) {
            set[1] /= GB;
            set[2] = "Gbit/s";
        } else if (set[1] > MB) {
            set[1] /= MB;
            set[2] = "Mbit/s";
        } else if (set[1] > KB) {
            set[1] /= KB;
            set[2] = "Kbit/s";
        } else {
            set[2] = "bit/s";
        }
        set[1] = Math.round(set[1] * 100) / 100;
        return set;
    },

    /**
     * Transform bytes in a perfdata set to a human readable value
     */
    perfdataCalcBytesReadable: function (set, kiloMultiplier = 1000) {
        const KB = kiloMultiplier;
        const MB = kiloMultiplier * kiloMultiplier;
        const GB = kiloMultiplier * kiloMultiplier * kiloMultiplier;
        if (set[1] > GB) {
            set[1] /= GB;
            set[2] = "GB/s";
        } else if (set[1] > MB) {
            set[1] /= MB;
            set[2] = "MB/s";
        } else if (set[1] > KB) {
            set[1] /= KB;
            set[2] = "KB/s";
        } else {
            set[2] = "B/s";
        }
        set[1] = Math.round(set[1] * 100) / 100;
        return set;
    },

    /**
     * Calculates the percentage usage of a line when the current value
     *  and the max value are given in the perfdata string
     */
    perfdataCalcPerc: function (set) {
        // Check if all needed information are present
        if (set[1] === null || set[6] === null || set[1] == "" || set[6] == "") return set;

        // Calculate percentages with 2 decimals and reset other options
        return Array(set[0], Math.round(((set[1] * 100) / set[6]) * 100) / 100, "%", set[3], set[4], 0, 100);
    },

    /**
     * Split perfdata into mutlidimensional array
     *      Each 1st dimension is a set of perfdata such as 'inUsage=19.34%;85;98')
     *      The 2nd dimension is each set broken apart (label, value, uom, etc.)
     *
     * Inspired by parsePerfdata function by Lars Michelsen which was
     * adapted from PNP process_perfdata.pl.  Thanks to Jörg Linge..
     * The function was originally taken from Nagios::Plugin::Performance
     * Thanks to Gavin Carr and Ton Voon
     *
     * @author      Greg Frater <greg@fraterfactory.com>
     *
     */
    parsePerfdataString: function () {
        const parsed = [];

        let perfdata = this.obj.conf.perfdata;
        if (!perfdata) return [];

        // Clean up perfdata
        perfdata = perfdata.replace(/\s*=\s*/, "=");

        // Break perfdata string into array of individual sets
        const re = /([^=]+)=([\d\.\-]+)([\w%]*);?([\d\.\-:~@]+)?;?([\d\.\-:~@]+)?;?([\d\.\-]+)?;?([\d\.\-]+)?\s*/g; // eslint-disable-line no-useless-escape
        const perfdataMatches = perfdata.match(re);

        // Check for empty perfdata
        if (perfdataMatches == null) return [];

        // Break perfdata parts into array
        for (let i = 0; i < perfdataMatches.length; i++) {
            // Get parts of perfdata from string
            const tmpSetMatches = perfdataMatches[i].match(
                /(&#145;)?([\w\s\=\'\-]*)(&#145;)?\=([\d\.\-\+]*)([\w%]*)[\;|\s]?([\d\.\-:~@]+)*[\;|\s]?([\d\.\-:~@]+)*[\;|\s]?([\d\.\-\+]*)[\;|\s]?([\d\.\-\+]*)/ // eslint-disable-line no-useless-escape
            );

            // Check if we got any perfdata
            if (tmpSetMatches === null) continue;

            parsed.push([
                tmpSetMatches[2], // label
                tmpSetMatches[4], // value
                tmpSetMatches[5], // UOM
                tmpSetMatches[6], // warn
                tmpSetMatches[7], // crit
                tmpSetMatches[8], // min
                tmpSetMatches[9] // max
            ]);
        }
        return parsed;
    },

    /*
     * There is a rectangle: viewport (current Leaflet map shown) + 10% on each side.
     * This function cuts the lines protruding beyond the rectangle.
     * Shortened lines prevent renderLine() from generating many vast <canvas> (10000-ish pixels) which eventually lead
     * to memory exhaustion or crash the browser
     *
     * Returns:
     *  either: [xA, yA, xB, yB] - original or adjusted line A/B coordinates,
     *  or: null - line out of a rectangle, don't render
     */

    cutLineBeyondViewport: function (xA, yA, xB, yB) {
        if (!g_map) return [xA, yA, xB, yB]; // no viewport - no clipping

        const viewport_size = g_map.getSize();
        const xMax = viewport_size.x; // 1920
        const yMax = viewport_size.y; // 1080
        const xTolerance = xMax * 0.95; // 1824 (95%)
        const yTolerance = yMax * 0.95; // 1026 (95%)

        // Cohen-Sutherland algorithm

        // region codes
        const INSIDE = 0; // 0000
        const LEFT = 1; // 0001
        const RIGHT = 2; // 0010
        const BOTTOM = 4; // 0100
        const TOP = 8; // 1000

        // boundaries
        const x_min = -xTolerance;
        const x_max = xMax + xTolerance;
        const y_min = -yTolerance;
        const y_max = yMax + yTolerance;

        // Function to compute region code
        const regionCode = function (x, y) {
            let code = INSIDE;

            if (x < x_min)
                // to the left of rectangle
                code |= LEFT;
            else if (x > x_max)
                // to the right of rectangle
                code |= RIGHT;
            if (y < y_min)
                // below the rectangle
                code |= BOTTOM;
            else if (y > y_max)
                // above the rectangle
                code |= TOP;

            return code;
        };

        const cohenSutherlandClip = function (x1, y1, x2, y2) {
            let code1 = regionCode(x1, y1);
            let code2 = regionCode(x2, y2);

            let accept = false;

            while (true) {
                if (code1 == 0 && code2 == 0) {
                    // If both endpoints lie within rectangle
                    accept = true;
                    break;
                } else if (code1 & code2) {
                    // If both endpoints are outside rectangle,
                    // in same region
                    break;
                } else {
                    // Some segment of line lies within the
                    // rectangle
                    let code_out;
                    let x, y;

                    // At least one endpoint is outside the
                    // rectangle, pick it.
                    if (code1 != 0) code_out = code1;
                    else code_out = code2;

                    // Find intersection point;
                    // using formulas y = y1 + slope * (x - x1),
                    // x = x1 + (1 / slope) * (y - y1)
                    if (code_out & TOP) {
                        // point is above the clip rectangle
                        x = x1 + ((x2 - x1) * (y_max - y1)) / (y2 - y1);
                        y = y_max;
                    } else if (code_out & BOTTOM) {
                        // point is below the rectangle
                        x = x1 + ((x2 - x1) * (y_min - y1)) / (y2 - y1);
                        y = y_min;
                    } else if (code_out & RIGHT) {
                        // point is to the right of rectangle
                        y = y1 + ((y2 - y1) * (x_max - x1)) / (x2 - x1);
                        x = x_max;
                    } else if (code_out & LEFT) {
                        // point is to the left of rectangle
                        y = y1 + ((y2 - y1) * (x_min - x1)) / (x2 - x1);
                        x = x_min;
                    }

                    // Now intersection point x,y is found
                    // We replace point outside rectangle
                    // by intersection point
                    if (code_out == code1) {
                        x1 = x;
                        y1 = y;
                        code1 = regionCode(x1, y1);
                    } else {
                        x2 = x;
                        y2 = y;
                        code2 = regionCode(x2, y2);
                    }
                }
            }
            if (accept) return [x1, y1, x2, y2];
            else return null;
        };

        return cohenSutherlandClip(xA, yA, xB, yB);
    }
});
