/*
 * Functionality for the CodeIgniter Debug Toolbar.
 */

var ciDebugBar = {
    toolbarContainer: null,
    toolbar: null,
    icon: null,

    init: function () {
        this.toolbarContainer = document.getElementById("toolbarContainer");
        this.toolbar = document.getElementById("debug-bar");
        this.icon = document.getElementById("debug-icon");

        ciDebugBar.createListeners();
        ciDebugBar.setToolbarState();
        ciDebugBar.setToolbarPosition();
        ciDebugBar.setToolbarTheme();
        ciDebugBar.toggleViewsHints();
        ciDebugBar.routerLink();
        ciDebugBar.setHotReloadState();

        document
            .getElementById("debug-bar-link")
            .addEventListener("click", ciDebugBar.toggleToolbar, true);
        document
            .getElementById("debug-icon-link")
            .addEventListener("click", ciDebugBar.toggleToolbar, true);

        historyLoad = this.toolbar.getElementsByClassName("ci-history-load");

        if (historyLoad.length) {
            // Allows highlighting the row of the current history request
            var btn = this.toolbar.querySelector(
                'button[data-time="' + localStorage.getItem("debugbar-time-new") + '"]'
            );
            ciDebugBar.addClass(btn.parentNode.parentNode, "current");


            for (var i = 0; i < historyLoad.length; i++) {
                historyLoad[i].addEventListener(
                    "click",
                    function () {
                        loadDoc(this.getAttribute("data-time"));
                    },
                    true
                );
            }
        }

        // Display the active Tab on page load
        var tab = ciDebugBar.readCookie("debug-bar-tab");
        if (document.getElementById(tab)) {
            var el = document.getElementById(tab);
            ciDebugBar.switchClass(el, "debug-bar-ndisplay", "debug-bar-dblock");
            ciDebugBar.addClass(el, "active");
            tab = document.querySelector("[data-tab=" + tab + "]");
            if (tab) {
                ciDebugBar.addClass(tab.parentNode, "active");
            }
        }
    },

    createListeners: function () {
        var buttons = [].slice.call(
            this.toolbar.querySelectorAll(".ci-label a")
        );

        for (var i = 0; i < buttons.length; i++) {
            buttons[i].addEventListener("click", ciDebugBar.showTab, true);
        }

        // Hook up generic toggle via data attributes `data-toggle="foo"`
        var links = this.toolbar.querySelectorAll("[data-toggle]");
        for (var i = 0; i < links.length; i++) {
            let toggleData = links[i].getAttribute("data-toggle");
            if (toggleData === "datatable") {

                let datatable = links[i].getAttribute("data-table");
                links[i].addEventListener("click", function() {
                    ciDebugBar.toggleDataTable(datatable)
                }, true);

            } else if (toggleData === "childrows") {

                let child = links[i].getAttribute("data-child");
                links[i].addEventListener("click", function() {
                    ciDebugBar.toggleChildRows(child)
                }, true);

            } else {
                links[i].addEventListener("click", ciDebugBar.toggleRows, true);
            }
        }
    },

    showTab: function () {
        // Get the target tab, if any
        var tab = document.getElementById(this.getAttribute("data-tab"));

        // If the label have not a tab stops here
        if (! tab) {
            return;
        }

        // Remove debug-bar-tab cookie
        ciDebugBar.createCookie("debug-bar-tab", "", -1);

        // Check our current state.
        var state = tab.classList.contains("debug-bar-dblock");

        // Hide all tabs
        var tabs = document.querySelectorAll("#debug-bar .tab");

        for (var i = 0; i < tabs.length; i++) {
            ciDebugBar.switchClass(tabs[i], "debug-bar-dblock", "debug-bar-ndisplay");
        }

        // Mark all labels as inactive
        var labels = document.querySelectorAll("#debug-bar .ci-label");

        for (var i = 0; i < labels.length; i++) {
            ciDebugBar.removeClass(labels[i], "active");
        }

        // Show/hide the selected tab
        if (! state) {
            ciDebugBar.switchClass(tab, "debug-bar-ndisplay", "debug-bar-dblock");
            ciDebugBar.addClass(this.parentNode, "active");
            // Create debug-bar-tab cookie to persistent state
            ciDebugBar.createCookie(
                "debug-bar-tab",
                this.getAttribute("data-tab"),
                365
            );
        }
    },

    addClass: function (el, className) {
        if (el.classList) {
            el.classList.add(className);
        } else {
            el.className += " " + className;
        }
    },

    removeClass: function (el, className) {
        if (el.classList) {
            el.classList.remove(className);
        } else {
            el.className = el.className.replace(
                new RegExp(
                    "(^|\\b)" + className.split(" ").join("|") + "(\\b|$)",
                    "gi"
                ),
                " "
            );
        }
    },

    switchClass  : function(el, classFrom, classTo) {
        ciDebugBar.removeClass(el, classFrom);
        ciDebugBar.addClass(el, classTo);
    },

    /**
     * Toggle display of another object based on
     * the data-toggle value of this object
     *
     * @param event
     */
    toggleRows: function (event) {
        if (event.target) {
            let row = event.target.closest("tr");
            let target = document.getElementById(
                row.getAttribute("data-toggle")
            );

            if (target.classList.contains("debug-bar-ndisplay")) {
                ciDebugBar.switchClass(target, "debug-bar-ndisplay", "debug-bar-dtableRow");
            } else {
                ciDebugBar.switchClass(target, "debug-bar-dtableRow", "debug-bar-ndisplay");
            }
        }
    },

    /**
     * Toggle display of a data table
     *
     * @param obj
     */
    toggleDataTable: function (obj) {
        if (typeof obj == "string") {
            obj = document.getElementById(obj + "_table");
        }

        if (obj) {
            if (obj.classList.contains("debug-bar-ndisplay")) {
                ciDebugBar.switchClass(obj, "debug-bar-ndisplay", "debug-bar-dblock");
            } else {
                ciDebugBar.switchClass(obj, "debug-bar-dblock", "debug-bar-ndisplay");
            }
        }
    },

    /**
     * Toggle display of timeline child elements
     *
     * @param obj
     */
    toggleChildRows: function (obj) {
        if (typeof obj == "string") {
            par = document.getElementById(obj + "_parent");
            obj = document.getElementById(obj + "_children");
        }

        if (par && obj) {

            if (obj.classList.contains("debug-bar-ndisplay")) {
                ciDebugBar.removeClass(obj, "debug-bar-ndisplay");
            } else {
                ciDebugBar.addClass(obj, "debug-bar-ndisplay");
            }

            par.classList.toggle("timeline-parent-open");
        }
    },

    //--------------------------------------------------------------------

    /**
     *   Toggle tool bar from full to icon and icon to full
     */
    toggleToolbar: function () {
        var open = ! ciDebugBar.toolbar.classList.contains("debug-bar-ndisplay");

        if (open) {
            ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-ndisplay", "debug-bar-dinlineBlock");
            ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-dinlineBlock", "debug-bar-ndisplay");
        } else {
            ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-dinlineBlock", "debug-bar-ndisplay");
            ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-ndisplay", "debug-bar-dinlineBlock");
        }

        // Remember it for other page loads on this site
        ciDebugBar.createCookie("debug-bar-state", "", -1);
        ciDebugBar.createCookie(
            "debug-bar-state",
            open == true ? "minimized" : "open",
            365
        );
    },

    /**
     * Sets the initial state of the toolbar (open or minimized) when
     * the page is first loaded to allow it to remember the state between refreshes.
     */
    setToolbarState: function () {
        var open = ciDebugBar.readCookie("debug-bar-state");

        if (open != "open") {
            ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-ndisplay", "debug-bar-dinlineBlock");
            ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-dinlineBlock", "debug-bar-ndisplay");
        } else {
            ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-dinlineBlock", "debug-bar-ndisplay");
            ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-ndisplay", "debug-bar-dinlineBlock");
        }
    },

    toggleViewsHints: function () {
        // Avoid toggle hints on history requests that are not the initial
        if (
            localStorage.getItem("debugbar-time") !=
            localStorage.getItem("debugbar-time-new")
        ) {
            var a = document.querySelector('a[data-tab="ci-views"]');
            a.href = "#";
            return;
        }

        var nodeList = []; // [ Element, NewElement( 1 )/OldElement( 0 ) ]
        var sortedComments = [];
        var comments = [];

        var getComments = function () {
            var nodes = [];
            var result = [];
            var xpathResults = document.evaluate(
                "//comment()[starts-with(., ' DEBUG-VIEW')]",
                document,
                null,
                XPathResult.ANY_TYPE,
                null
            );
            var nextNode = xpathResults.iterateNext();
            while (nextNode) {
                nodes.push(nextNode);
                nextNode = xpathResults.iterateNext();
            }

            // sort comment by opening and closing tags
            for (var i = 0; i < nodes.length; ++i) {
                // get file path + name to use as key
                var path = nodes[i].nodeValue.substring(
                    18,
                    nodes[i].nodeValue.length - 1
                );

                if (nodes[i].nodeValue[12] === "S") {
                    // simple check for start comment
                    // create new entry
                    result[path] = [nodes[i], null];
                } else if (result[path]) {
                    // add to existing entry
                    result[path][1] = nodes[i];
                }
            }

            return result;
        };

        // find node that has TargetNode as parentNode
        var getParentNode = function (node, targetNode) {
            if (node.parentNode === null) {
                return null;
            }

            if (node.parentNode !== targetNode) {
                return getParentNode(node.parentNode, targetNode);
            }

            return node;
        };

        // define invalid & outer ( also invalid ) elements
        const INVALID_ELEMENTS = ["NOSCRIPT", "SCRIPT", "STYLE"];
        const OUTER_ELEMENTS = ["HTML", "BODY", "HEAD"];

        var getValidElementInner = function (node, reverse) {
            // handle null node
            if (node === null) {
                return null;
            }

            // handle invalid tags
            if (OUTER_ELEMENTS.indexOf(node.nodeName) !== -1) {
                for (var i = 0; i < document.body.children.length; ++i) {
                    var index = reverse
                        ? document.body.children.length - (i + 1)
                        : i;
                    var element = document.body.children[index];

                    // skip invalid tags
                    if (INVALID_ELEMENTS.indexOf(element.nodeName) !== -1) {
                        continue;
                    }

                    return [element, reverse];
                }

                return null;
            }

            // get to next valid element
            while (
                node !== null &&
                INVALID_ELEMENTS.indexOf(node.nodeName) !== -1
            ) {
                node = reverse
                    ? node.previousElementSibling
                    : node.nextElementSibling;
            }

            // return non array if we couldnt find something
            if (node === null) {
                return null;
            }

            return [node, reverse];
        };

        // get next valid element ( to be safe to add divs )
        // @return [ element, skip element ] or null if we couldnt find a valid place
        var getValidElement = function (nodeElement) {
            if (nodeElement) {
                if (nodeElement.nextElementSibling !== null) {
                    return (
                        getValidElementInner(
                            nodeElement.nextElementSibling,
                            false
                        ) ||
                        getValidElementInner(
                            nodeElement.previousElementSibling,
                            true
                        )
                    );
                }
                if (nodeElement.previousElementSibling !== null) {
                    return getValidElementInner(
                        nodeElement.previousElementSibling,
                        true
                    );
                }
            }

            // something went wrong! -> element is not in DOM
            return null;
        };

        function showHints() {
            // Had AJAX? Reset view blocks
            sortedComments = getComments();

            for (var key in sortedComments) {
                var startElement = getValidElement(sortedComments[key][0]);
                var endElement = getValidElement(sortedComments[key][1]);

                // skip if we couldnt get a valid element
                if (startElement === null || endElement === null) {
                    continue;
                }

                // find element which has same parent as startelement
                var jointParent = getParentNode(
                    endElement[0],
                    startElement[0].parentNode
                );
                if (jointParent === null) {
                    // find element which has same parent as endelement
                    jointParent = getParentNode(
                        startElement[0],
                        endElement[0].parentNode
                    );
                    if (jointParent === null) {
                        // both tries failed
                        continue;
                    } else {
                        startElement[0] = jointParent;
                    }
                } else {
                    endElement[0] = jointParent;
                }

                var debugDiv = document.createElement("div"); // holder
                var debugPath = document.createElement("div"); // path
                var childArray = startElement[0].parentNode.childNodes; // target child array
                var parent = startElement[0].parentNode;
                let start, end;

                // setup container
                debugDiv.classList.add("debug-view");
                debugDiv.classList.add("show-view");
                debugPath.classList.add("debug-view-path");
                debugPath.innerText = key;
                debugDiv.appendChild(debugPath);

                // calc distance between them
                // start
                for (var i = 0; i < childArray.length; ++i) {
                    // check for comment ( start & end ) -> if its before valid start element
                    if (
                        childArray[i] === sortedComments[key][1] ||
                        childArray[i] === sortedComments[key][0] ||
                        childArray[i] === startElement[0]
                    ) {
                        start = i;
                        if (childArray[i] === sortedComments[key][0]) {
                            start++; // increase to skip the start comment
                        }
                        break;
                    }
                }
                // adjust if we want to skip the start element
                if (startElement[1]) {
                    start++;
                }

                // end
                for (var i = start; i < childArray.length; ++i) {
                    if (childArray[i] === endElement[0]) {
                        end = i;
                        // dont break to check for end comment after end valid element
                    } else if (childArray[i] === sortedComments[key][1]) {
                        // if we found the end comment, we can break
                        end = i;
                        break;
                    }
                }

                // move elements
                var number = end - start;
                if (endElement[1]) {
                    number++;
                }
                for (var i = 0; i < number; ++i) {
                    if (INVALID_ELEMENTS.indexOf(childArray[start]) !== -1) {
                        // skip invalid childs that can cause problems if moved
                        start++;
                        continue;
                    }
                    debugDiv.appendChild(childArray[start]);
                }

                // add container to DOM
                nodeList.push(parent.insertBefore(debugDiv, childArray[start]));
            }

            ciDebugBar.createCookie("debug-view", "show", 365);
            ciDebugBar.addClass(btn, "active");
        }

        function hideHints() {
            for (var i = 0; i < nodeList.length; ++i) {
                var index;

                // find index
                for (
                    var j = 0;
                    j < nodeList[i].parentNode.childNodes.length;
                    ++j
                ) {
                    if (nodeList[i].parentNode.childNodes[j] === nodeList[i]) {
                        index = j;
                        break;
                    }
                }

                // move child back
                while (nodeList[i].childNodes.length !== 1) {
                    nodeList[i].parentNode.insertBefore(
                        nodeList[i].childNodes[1],
                        nodeList[i].parentNode.childNodes[index].nextSibling
                    );
                    index++;
                }

                nodeList[i].parentNode.removeChild(nodeList[i]);
            }
            nodeList.length = 0;

            ciDebugBar.createCookie("debug-view", "", -1);
            ciDebugBar.removeClass(btn, "active");
        }

        var btn = document.querySelector("[data-tab=ci-views]");

        // If the Views Collector is inactive stops here
        if (! btn) {
            return;
        }

        btn.parentNode.onclick = function () {
            if (ciDebugBar.readCookie("debug-view")) {
                hideHints();
            } else {
                showHints();
            }
        };

        // Determine Hints state on page load
        if (ciDebugBar.readCookie("debug-view")) {
            showHints();
        }
    },

    setToolbarPosition: function () {
        var btnPosition = this.toolbar.querySelector("#toolbar-position");

        if (ciDebugBar.readCookie("debug-bar-position") === "top") {
            ciDebugBar.addClass(ciDebugBar.icon, "fixed-top");
            ciDebugBar.addClass(ciDebugBar.toolbar, "fixed-top");
        }

        btnPosition.addEventListener(
            "click",
            function () {
                var position = ciDebugBar.readCookie("debug-bar-position");

                ciDebugBar.createCookie("debug-bar-position", "", -1);

                if (! position || position === "bottom") {
                    ciDebugBar.createCookie("debug-bar-position", "top", 365);
                    ciDebugBar.addClass(ciDebugBar.icon, "fixed-top");
                    ciDebugBar.addClass(ciDebugBar.toolbar, "fixed-top");
                } else {
                    ciDebugBar.createCookie(
                        "debug-bar-position",
                        "bottom",
                        365
                    );
                    ciDebugBar.removeClass(ciDebugBar.icon, "fixed-top");
                    ciDebugBar.removeClass(ciDebugBar.toolbar, "fixed-top");
                }
            },
            true
        );
    },

    setToolbarTheme: function () {
        var btnTheme = this.toolbar.querySelector("#toolbar-theme");
        var isDarkMode = window.matchMedia(
            "(prefers-color-scheme: dark)"
        ).matches;
        var isLightMode = window.matchMedia(
            "(prefers-color-scheme: light)"
        ).matches;

        // If a cookie is set with a value, we force the color scheme
        if (ciDebugBar.readCookie("debug-bar-theme") === "dark") {
            ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "light");
            ciDebugBar.addClass(ciDebugBar.toolbarContainer, "dark");
        } else if (ciDebugBar.readCookie("debug-bar-theme") === "light") {
            ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "dark");
            ciDebugBar.addClass(ciDebugBar.toolbarContainer, "light");
        }

        btnTheme.addEventListener(
            "click",
            function () {
                var theme = ciDebugBar.readCookie("debug-bar-theme");

                if (
                    ! theme &&
                    window.matchMedia("(prefers-color-scheme: dark)").matches
                ) {
                    // If there is no cookie, and "prefers-color-scheme" is set to "dark"
                    // It means that the user wants to switch to light mode
                    ciDebugBar.createCookie("debug-bar-theme", "light", 365);
                    ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "dark");
                    ciDebugBar.addClass(ciDebugBar.toolbarContainer, "light");
                } else {
                    if (theme === "dark") {
                        ciDebugBar.createCookie(
                            "debug-bar-theme",
                            "light",
                            365
                        );
                        ciDebugBar.removeClass(
                            ciDebugBar.toolbarContainer,
                            "dark"
                        );
                        ciDebugBar.addClass(
                            ciDebugBar.toolbarContainer,
                            "light"
                        );
                    } else {
                        // In any other cases: if there is no cookie, or the cookie is set to
                        // "light", or the "prefers-color-scheme" is "light"...
                        ciDebugBar.createCookie("debug-bar-theme", "dark", 365);
                        ciDebugBar.removeClass(
                            ciDebugBar.toolbarContainer,
                            "light"
                        );
                        ciDebugBar.addClass(
                            ciDebugBar.toolbarContainer,
                            "dark"
                        );
                    }
                }
            },
            true
        );
    },

    setHotReloadState: function () {
        var btn = document.getElementById("debug-hot-reload").parentNode;
        var btnImg = btn.getElementsByTagName("img")[0];
        var eventSource;

        // If the Hot Reload Collector is inactive stops here
        if (! btn) {
            return;
        }

        btn.onclick = function () {
            if (ciDebugBar.readCookie("debug-hot-reload")) {
                ciDebugBar.createCookie("debug-hot-reload", "", -1);
                ciDebugBar.removeClass(btn, "active");
                ciDebugBar.removeClass(btnImg, "rotate");

                // Close the EventSource connection if it exists
                if (typeof eventSource !== "undefined") {
                    eventSource.close();
                    eventSource = void 0; // Undefine the variable
                }
            } else {
                ciDebugBar.createCookie("debug-hot-reload", "show", 365);
                ciDebugBar.addClass(btn, "active");
                ciDebugBar.addClass(btnImg, "rotate");

                eventSource = ciDebugBar.hotReloadConnect();
            }
        };

        // Determine Hot Reload state on page load
        if (ciDebugBar.readCookie("debug-hot-reload")) {
            ciDebugBar.addClass(btn, "active");
            ciDebugBar.addClass(btnImg, "rotate");
            eventSource = ciDebugBar.hotReloadConnect();
        }
    },

    hotReloadConnect: function () {
        const eventSource = new EventSource(ciSiteURL + "/__hot-reload");

        eventSource.addEventListener("reload", function (e) {
            console.log("reload", e);
            window.location.reload();
        });

        eventSource.onerror = (err) => {
            console.error("EventSource failed:", err);
        };

        return eventSource;
    },

    /**
     * Helper to create a cookie.
     *
     * @param name
     * @param value
     * @param days
     */
    createCookie: function (name, value, days) {
        if (days) {
            var date = new Date();

            date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);

            var expires = "; expires=" + date.toGMTString();
        } else {
            var expires = "";
        }

        document.cookie =
            name + "=" + value + expires + "; path=/; samesite=Lax";
    },

    readCookie: function (name) {
        var nameEQ = name + "=";
        var ca = document.cookie.split(";");

        for (var i = 0; i < ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) == " ") {
                c = c.substring(1, c.length);
            }
            if (c.indexOf(nameEQ) == 0) {
                return c.substring(nameEQ.length, c.length);
            }
        }
        return null;
    },

    trimSlash: function (text) {
        return text.replace(/^\/|\/$/g, "");
    },

    routerLink: function () {
        var row, _location;
        var rowGet = this.toolbar.querySelectorAll(
            'td[data-debugbar-route="GET"]'
        );
        var patt = /\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/;

        for (var i = 0; i < rowGet.length; i++) {
            row = rowGet[i];
            if (!/\/\(.+?\)/.test(rowGet[i].innerText)) {
                ciDebugBar.addClass(row, "debug-bar-pointer");
                row.setAttribute(
                    "title",
                    location.origin + "/" + ciDebugBar.trimSlash(row.innerText)
                );
                row.addEventListener("click", function (ev) {
                    _location =
                        location.origin +
                        "/" +
                        ciDebugBar.trimSlash(ev.target.innerText);
                    var redirectWindow = window.open(_location, "_blank");
                    redirectWindow.location;
                });
            } else {
                row.innerHTML =
                    "<div>" +
                    row.innerText +
                    "</div>" +
                    '<form data-debugbar-route-tpl="' +
                    ciDebugBar.trimSlash(row.innerText.replace(patt, "?")) +
                    '">' +
                    row.innerText.replace(
                        patt,
                        '<input id="debugbar-route-id-' + i + '" type="text" placeholder="$1">'
                    ) +
                    '<input type="submit" value="Go" class="debug-bar-mleft4">' +
                    "</form>";
            }
        }

        rowGet = this.toolbar.querySelectorAll(
            'td[data-debugbar-route="GET"] form'
        );
        for (var i = 0; i < rowGet.length; i++) {
            row = rowGet[i];

            row.addEventListener("submit", function (event) {
                event.preventDefault();
                var inputArray = [],
                    t = 0;
                var input = event.target.querySelectorAll("input[type=text]");
                var tpl = event.target.getAttribute("data-debugbar-route-tpl");

                for (var n = 0; n < input.length; n++) {
                    if (input[n].value.length > 0) {
                        inputArray.push(input[n].value);
                    }
                }

                if (inputArray.length > 0) {
                    _location =
                        location.origin +
                        "/" +
                        tpl.replace(/\?/g, function () {
                            return inputArray[t++];
                        });

                    var redirectWindow = window.open(_location, "_blank");
                    redirectWindow.location;
                }
            });
        }
    },
};
