/**
 * Copyright JS Foundation and other contributors, http://js.foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/

RED.palette = (function() {

    var exclusion = ['config','unknown','deprecated'];
    var coreCategories = [
        'subflows',
        'common',
        'function',
        'network',
        'input',
        'output',
        'sequence',
        'parser',
        'storage',
        'analysis',
        'social',
        'advanced'
    ];

    var categoryContainers = {};
    var sidebarControls;

    let paletteState = { filter: "", collapsed: [] };

    let filterRefreshTimeout

    function createCategory(originalCategory,rootCategory,category,ns) {
        if ($("#red-ui-palette-base-category-"+rootCategory).length === 0) {
            createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory);
        }
        $("#red-ui-palette-container-"+rootCategory).show();
        if ($("#red-ui-palette-"+category).length === 0) {
            $("#red-ui-palette-base-category-"+rootCategory).append('<div id="red-ui-palette-'+category+'"></div>');
        }
    }
    function createCategoryContainer(originalCategory,category, labelId) {
        var label = RED._(labelId, {defaultValue:category});
        label = (label || category).replace(/_/g, " ");
        var catDiv = $('<div id="red-ui-palette-container-'+category+'" class="red-ui-palette-category hide">'+
            '<div id="red-ui-palette-header-'+category+'" class="red-ui-palette-header"><i class="expanded fa fa-angle-down"></i><span>'+label+'</span></div>'+
            '<div class="red-ui-palette-content" id="red-ui-palette-base-category-'+category+'">'+
            '<div id="red-ui-palette-'+category+'"></div>'+
            '<div id="red-ui-palette-'+category+'-input"></div>'+
            '<div id="red-ui-palette-'+category+'-output"></div>'+
            '<div id="red-ui-palette-'+category+'-function"></div>'+
            '</div>'+
            '</div>').appendTo("#red-ui-palette-container");
        catDiv.data('category',originalCategory);
        catDiv.data('label',label);
        categoryContainers[category] = {
            container: catDiv,
            hide: function (instant) {
                if (instant) {
                    catDiv.hide()
                } else {
                    catDiv.slideUp()
                }
            },
            show: function () {
                catDiv.show()
            },
            isOpen: function () {
                return !!catDiv.hasClass("red-ui-palette-open")
            },
            getNodeCount: function (visibleOnly) {
                const nodes = catDiv.find(".red-ui-palette-node")
                if (visibleOnly) {
                    return nodes.filter(function() { return $(this).attr("data-filter") !== "true"}).length
                } else {
                    return nodes.length
                }
            },
            close: function(instant, skipSaveState) {
                catDiv.removeClass("red-ui-palette-open");
                catDiv.addClass("red-ui-palette-closed");
                if (instant) {
                    $("#red-ui-palette-base-category-"+category).hide();
                } else {
                    $("#red-ui-palette-base-category-"+category).slideUp();
                }
                $("#red-ui-palette-header-"+category+" i").removeClass("expanded");
                if (!skipSaveState) {
                    if (!paletteState.collapsed.includes(category)) {
                        paletteState.collapsed.push(category);
                        savePaletteState();
                    }
                }
            },
            open: function(skipSaveState) {
                catDiv.addClass("red-ui-palette-open");
                catDiv.removeClass("red-ui-palette-closed");
                $("#red-ui-palette-base-category-"+category).slideDown();
                $("#red-ui-palette-header-"+category+" i").addClass("expanded");
                if (!skipSaveState) {
                    if (paletteState.collapsed.includes(category)) {
                        paletteState.collapsed.splice(paletteState.collapsed.indexOf(category), 1);
                        savePaletteState();
                    }
                }
            },
            toggle: function() {
                if (categoryContainers[category].isOpen()) {
                    categoryContainers[category].close();
                } else {
                    categoryContainers[category].open();
                }
            }
        };

        $("#red-ui-palette-header-"+category).on('click', function(e) {
            categoryContainers[category].toggle();
        });
    }

    function setLabel(type, el,label, info) {
        var nodeWidth = 82;
        var nodeHeight = 25;
        var lineHeight = 20;
        var portHeight = 10;

        el.attr("data-palette-label",label);

        label = RED.utils.sanitize(label);


        var words = label.split(/([ -]|\\n )/);

        var displayLines = [];

        var currentLine = "";
        for (var i=0;i<words.length;i++) {
            var word = words[i];
            if (word === "\\n ") {
                displayLines.push(currentLine);
                currentLine = "";
                continue;
            }
            var sep = (i == 0) ? "" : " ";
            var newWidth = RED.view.calculateTextWidth(currentLine+sep+word, "red-ui-palette-label");
            if (newWidth < nodeWidth) {
                currentLine += sep +word;
            } else {
                if (i > 0) {
                    displayLines.push(currentLine);
                }
                while (true) {
                    var wordWidth = RED.view.calculateTextWidth(word, "red-ui-palette-label");
                    if (wordWidth >= nodeWidth) {
                        // break word if too wide
                        for(var j = word.length; j > 0; j--) {
                            var s = word.substring(0, j);
                            var width = RED.view.calculateTextWidth(s, "red-ui-palette-label");
                            if (width < nodeWidth) {
                                displayLines.push(s);
                                word = word.substring(j);
                                break;
                            }
                        }
                    }
                    else {
                        currentLine = word;
                        break;
                    }
                }
            }
        }
        displayLines.push(currentLine);

        var lines = displayLines.join("<br/>");
        var multiLineNodeHeight = 8+(lineHeight*displayLines.length);
        el.css({height:multiLineNodeHeight+"px"});

        var labelElement = el.find(".red-ui-palette-label");
        labelElement.html(lines).attr('dir', RED.text.bidi.resolveBaseTextDir(lines));

        el.find(".red-ui-palette-port").css({top:(multiLineNodeHeight/2-5)+"px"});

        var popOverContent;
        try {
            var l = "<p><b>"+RED.text.bidi.enforceTextDirectionWithUCC(label)+"</b></p>";
            popOverContent = $('<div></div>').append($(l+(info?info:RED.nodes.getNodeHelp(type)||"<p>"+RED._("palette.noInfo")+"</p>").trim())
                                .filter(function(n) {
                                    return (this.nodeType == 1 && this.nodeName == "P") || (this.nodeType == 3 && this.textContent.trim().length > 0)
                                }).slice(0,2));
            popOverContent.find("a").each(function(){
                var linkText = $(this).text();
                $(this).before(linkText);
                $(this).remove();
            });

            var typeInfo = RED.nodes.getType(type);

            if (typeInfo) {
                var metaData = "";
                if (typeInfo && !/^subflow:/.test(type)) {
                    metaData = typeInfo.set.module+" : ";
                }
                metaData += type;

                const safeType = type.replace(/'/g,"\\'");
                const searchType = type.indexOf(' ') > -1 ? '&quot;' + type + '&quot;' : type

                if (/^subflow:/.test(type)) {
                    $('<button type="button" onclick="RED.workspaces.show(\''+type.substring(8).replace(/'/g,"\\'")+'\'); return false;" class="red-ui-button red-ui-button-small" style="float: right; margin-left: 5px;"><i class="fa fa-pencil"></i></button>').appendTo(popOverContent)
                }

                $('<button type="button" onclick="RED.search.show(\'type:'+searchType+'\'); return false;" class="red-ui-button red-ui-button-small" style="float: right; margin-left: 5px;"><i class="fa fa-search"></i></button>').appendTo(popOverContent)

                $('<button type="button" onclick="RED.sidebar.help.show(\''+safeType+'\'); return false;" class="red-ui-button red-ui-button-small" style="float: right; margin-left: 5px;"><i class="fa fa-book"></i></button>').appendTo(popOverContent)

                $('<p>',{style:"font-size: 0.8em"}).text(metaData).appendTo(popOverContent);
            }
        } catch(err) {
            // Malformed HTML may cause errors. TODO: need to understand what can break
            // NON-NLS: internal debug
            console.log("Error generating pop-over label for ",type);
            console.log(err.toString());
            popOverContent = "<p><b>"+label+"</b></p><p>"+RED._("palette.noInfo")+"</p>";
        }

        el.data('popover').setContent(popOverContent);
    }

    function setIcon(element,sf) {
        var icon_url = RED.utils.getNodeIcon(sf._def);
        var iconContainer = element.find(".red-ui-palette-icon-container");
        var currentIcon = iconContainer.attr("data-palette-icon");
        if (currentIcon !== icon_url) {
            iconContainer.attr("data-palette-icon", icon_url);
            RED.utils.createIconElement(icon_url, iconContainer, true);
        }
    }

    function getPaletteNode(type) {
        return $(".red-ui-palette-node[data-palette-type='"+type+"']");
    }

    function escapeCategory(category) {
        return category.replace(/[\x00-\x2c\x2e-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]/g,"_");
    }
    function addNodeType(nt,def) {
        if (getPaletteNode(nt).length) {
            return;
        }
        var nodeCategory = def.category;

        if (exclusion.indexOf(nodeCategory)===-1) {

            var originalCategory = nodeCategory;
            var category = escapeCategory(nodeCategory);
            var rootCategory = category.split("-")[0];

            var d = $('<div>',{class:"red-ui-palette-node"}).attr("data-palette-type",nt).data('category',rootCategory);

            var label = RED.utils.getPaletteLabel(nt, def);///^(.*?)([ -]in|[ -]out)?$/.exec(nt)[1];

            $('<div/>', {
                class: "red-ui-palette-label"+(((!def.align && def.inputs !== 0 && def.outputs === 0) || "right" === def.align) ? " red-ui-palette-label-right" : "")
            }).appendTo(d);


            if (def.icon) {
                var icon_url = RED.utils.getNodeIcon(def);
                var iconContainer = $('<div/>', {
                    class: "red-ui-palette-icon-container"+(((!def.align && def.inputs !== 0 && def.outputs === 0) || "right" === def.align) ? " red-ui-palette-icon-container-right" : "")
                }).appendTo(d);
                iconContainer.attr("data-palette-icon", icon_url);
                RED.utils.createIconElement(icon_url, iconContainer, true);
            }

            d.css("backgroundColor", RED.utils.getNodeColor(nt,def));

            if (def.outputs > 0) {
                var portOut = document.createElement("div");
                portOut.className = "red-ui-palette-port red-ui-palette-port-output";
                d.append(portOut);
            }

            if (def.inputs > 0) {
                var portIn = document.createElement("div");
                portIn.className = "red-ui-palette-port red-ui-palette-port-input";
                d.append(portIn);
            }

            createCategory(nodeCategory,rootCategory,category,(coreCategories.indexOf(rootCategory) !== -1)?"node-red":def.set.id);

            $("#red-ui-palette-"+category).append(d);

            d.on("mousedown", function(e) { e.preventDefault();});

            var popover = RED.popover.create({
                target:d,
                trigger: "hover",
                interactive: true,
                width: "300px",
                content: "hi",
                delay: { show: 750, hide: 50 }
            });

            d.data('popover',popover);

            var chart = $("#red-ui-workspace-chart");
            var chartSVG = $("#red-ui-workspace-chart>svg").get(0);
            var activeSpliceLink;
            var mouseX;
            var mouseY;
            var spliceTimer;
            var groupTimer;
            var activeGroup;
            var hoverGroup;
            var paletteWidth;
            var paletteTop;
            var dropEnabled;
            $(d).draggable({
                helper: 'clone',
                appendTo: '#red-ui-editor',
                revert: 'invalid',
                revertDuration: 200,
                containment:'#red-ui-main-container',
                start: function() {
                    dropEnabled = !(RED.nodes.workspace(RED.workspaces.active())?.locked);
                    paletteWidth = $("#red-ui-palette").width();
                    paletteTop = $("#red-ui-palette").parent().position().top + $("#red-ui-palette-container").position().top;
                    hoverGroup = null;
                    activeGroup = RED.view.getActiveGroup();
                    if (activeGroup) {
                        document.getElementById("group_select_"+activeGroup.id).classList.add("red-ui-flow-group-active-hovered");
                    }
                    RED.view.focus();
                },
                stop: function() {
                    if (dropEnabled) {
                        d3.select('.red-ui-flow-link-splice').classed('red-ui-flow-link-splice',false);
                        if (hoverGroup) {
                            document.getElementById("group_select_"+hoverGroup.id).classList.remove("red-ui-flow-group-hovered");
                        }
                        if (activeGroup) {
                            document.getElementById("group_select_"+activeGroup.id).classList.remove("red-ui-flow-group-active-hovered");
                        }
                        if (spliceTimer) { clearTimeout(spliceTimer); spliceTimer = null; }
                        if (groupTimer) { clearTimeout(groupTimer); groupTimer = null; }
                    }
                },
                drag: function(e,ui) {
                    var paletteNode = getPaletteNode(nt);
                    ui.originalPosition.left = paletteNode.offset().left;
                    if (dropEnabled) {
                        mouseX = ui.position.left - paletteWidth + (ui.helper.width()/2) + chart.scrollLeft();
                        mouseY = ui.position.top - paletteTop + (ui.helper.height()/2) + chart.scrollTop() + 10;
                        if (!groupTimer) {
                            groupTimer = setTimeout(function() {
                                var mx = mouseX / RED.view.scale();
                                var my = mouseY / RED.view.scale();
                                var group = RED.view.getGroupAtPoint(mx,my);
                                if (group !== hoverGroup) {
                                    if (hoverGroup) {
                                        document.getElementById("group_select_"+hoverGroup.id).classList.remove("red-ui-flow-group-hovered");
                                    }
                                    if (group) {
                                        document.getElementById("group_select_"+group.id).classList.add("red-ui-flow-group-hovered");
                                    }
                                    hoverGroup = group;
                                    if (hoverGroup) {
                                        $(ui.helper).data('group',hoverGroup);
                                    } else {
                                        $(ui.helper).removeData('group');
                                    }
                                }
                                groupTimer = null;

                            },200)
                        }
                        if (def.inputs > 0 && def.outputs > 0) {
                            if (!spliceTimer) {
                                spliceTimer = setTimeout(function() {
                                    var nodes = [];
                                    var bestDistance = Infinity;
                                    var bestLink = null;
                                    if (chartSVG.getIntersectionList) {
                                        var svgRect = chartSVG.createSVGRect();
                                        svgRect.x = mouseX;
                                        svgRect.y = mouseY;
                                        svgRect.width = 1;
                                        svgRect.height = 1;
                                        nodes = chartSVG.getIntersectionList(svgRect,chartSVG);
                                    } else {
                                        // Firefox doesn't do getIntersectionList and that
                                        // makes us sad
                                        nodes = RED.view.getLinksAtPoint(mouseX / RED.view.scale(), mouseY / RED.view.scale());
                                    }
                                    var mx = mouseX / RED.view.scale();
                                    var my = mouseY / RED.view.scale();
                                    for (var i=0;i<nodes.length;i++) {
                                        var node = d3.select(nodes[i]);
                                        if (node.classed('red-ui-flow-link-background') && !node.classed('red-ui-flow-link-link')) {
                                            var length = nodes[i].getTotalLength();
                                            for (var j=0;j<length;j+=10) {
                                                var p = nodes[i].getPointAtLength(j);
                                                var d2 = ((p.x-mx)*(p.x-mx))+((p.y-my)*(p.y-my));
                                                if (d2 < 200 && d2 < bestDistance) {
                                                    bestDistance = d2;
                                                    bestLink = nodes[i];
                                                }
                                            }
                                        }
                                    }
                                    if (activeSpliceLink && activeSpliceLink !== bestLink) {
                                        d3.select(activeSpliceLink.parentNode).classed('red-ui-flow-link-splice',false);
                                    }
                                    if (bestLink) {
                                        d3.select(bestLink.parentNode).classed('red-ui-flow-link-splice',true)
                                    } else {
                                        d3.select('.red-ui-flow-link-splice').classed('red-ui-flow-link-splice',false);
                                    }
                                    if (activeSpliceLink !== bestLink) {
                                        if (bestLink) {
                                            $(ui.helper).data('splice',d3.select(bestLink).data()[0]);
                                        } else {
                                            $(ui.helper).removeData('splice');
                                        }
                                    }
                                    activeSpliceLink = bestLink;
                                    spliceTimer = null;
                                },200);
                            }
                        }
                    }
                }
            });

            var nodeInfo = null;
            if (nt.indexOf("subflow:") === 0) {
                d.on("dblclick", function(e) {
                    RED.workspaces.show(nt.substring(8));
                    e.preventDefault();
                });
                var subflow = RED.nodes.subflow(nt.substring(8));
                nodeInfo = RED.utils.renderMarkdown(subflow.info||"");
            }
            setLabel(nt,d,label,nodeInfo);

            var categoryNode = $("#red-ui-palette-container-"+rootCategory);
            if (categoryNode.find(".red-ui-palette-node").length === 1) {
                if (!paletteState?.collapsed?.includes(rootCategory)) {
                    categoryContainers[rootCategory].open();
                } else {
                    categoryContainers[rootCategory].close(true);
                }
            }
            clearTimeout(filterRefreshTimeout)
            filterRefreshTimeout = setTimeout(() => {
                refreshFilter()
            }, 200)

        }
    }

    function removeNodeType(nt) {
        var paletteNode = getPaletteNode(nt);
        var categoryNode = paletteNode.closest(".red-ui-palette-category");
        paletteNode.remove();
        if (categoryNode.find(".red-ui-palette-node").length === 0) {
            if (categoryNode.find("i").hasClass("expanded")) {
                categoryNode.find(".red-ui-palette-content").slideToggle();
                categoryNode.find("i").toggleClass("expanded");
            }
            categoryNode.hide();
        }
    }

    function hideNodeType(nt) {
        var paletteNode = getPaletteNode(nt);
        paletteNode.hide();
        var categoryNode = paletteNode.closest(".red-ui-palette-category");
        var cl = categoryNode.find(".red-ui-palette-node");
        var c = 0;
        for (var i = 0; i < cl.length; i++) {
            if ($(cl[i]).css('display') === 'none') { c += 1; }
        }
        if (c === cl.length) { categoryNode.hide(); }
    }

    function showNodeType(nt) {
        var paletteNode = getPaletteNode(nt);
        var categoryNode = paletteNode.closest(".red-ui-palette-category");
        categoryNode.show();
        paletteNode.show();
    }
    function refreshNodeTypes() {
        RED.nodes.eachSubflow(refreshSubflow)
    }
    function refreshSubflow(sf) {
        var paletteNode = getPaletteNode('subflow:'+sf.id);
        var portInput = paletteNode.find(".red-ui-palette-port-input");
        var portOutput = paletteNode.find(".red-ui-palette-port-output");

        var paletteLabel = paletteNode.find(".red-ui-palette-label");
        paletteLabel.attr("class","red-ui-palette-label" + (((!sf._def.align && sf.in.length !== 0 && sf.out.length === 0) || "right" === sf._def.align) ? " red-ui-palette-label-right" : ""));

        var paletteIconContainer = paletteNode.find(".red-ui-palette-icon-container");
        paletteIconContainer.attr("class","red-ui-palette-icon-container" + (((!sf._def.align && sf.in.length !== 0 && sf.out.length === 0) || "right" === sf._def.align) ? " red-ui-palette-icon-container-right" : ""));

        if (portInput.length === 0 && sf.in.length > 0) {
            var portIn = document.createElement("div");
            portIn.className = "red-ui-palette-port red-ui-palette-port-input";
            paletteNode.append(portIn);
        } else if (portInput.length !== 0 && sf.in.length === 0) {
            portInput.remove();
        }

        if (portOutput.length === 0 && sf.out.length > 0) {
            var portOut = document.createElement("div");
            portOut.className = "red-ui-palette-port red-ui-palette-port-output";
            paletteNode.append(portOut);
        } else if (portOutput.length !== 0 && sf.out.length === 0) {
            portOutput.remove();
        }
        var currentLabel = paletteNode.attr("data-palette-label");
        var currentInfo = paletteNode.attr("data-palette-info");

        if (currentLabel !== sf.name || currentInfo !== sf.info || sf.in.length > 0 || sf.out.length > 0) {
            paletteNode.attr("data-palette-info",sf.info);
            setLabel(sf.type+":"+sf.id,paletteNode,sf.name,RED.utils.renderMarkdown(sf.info||""));
        }
        setIcon(paletteNode,sf);

        var currentCategory = paletteNode.data('category');
        var newCategory = (sf.category||"subflows");
        if (currentCategory !== newCategory) {
            var category = escapeCategory(newCategory);
            createCategory(newCategory,category,category,"node-red");

            var currentCategoryNode = paletteNode.closest(".red-ui-palette-category");
            var newCategoryNode = $("#red-ui-palette-"+category);
            newCategoryNode.append(paletteNode);
            if (newCategoryNode.find(".red-ui-palette-node").length === 1) {
                categoryContainers[category].open();
            }

            paletteNode.data('category',newCategory);
            if (currentCategoryNode.find(".red-ui-palette-node").length === 0) {
                if (currentCategoryNode.find("i").hasClass("expanded")) {
                    currentCategoryNode.find(".red-ui-palette-content").slideToggle();
                    currentCategoryNode.find("i").toggleClass("expanded");
                }
                currentCategoryNode.hide();
            }
        }

        paletteNode.css("backgroundColor", RED.utils.getNodeColor("subflow", sf._def));
    }

    function refreshFilter() {
        const val = $("#red-ui-palette-search input").val()
        var re = new RegExp(val.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),'i');
        $("#red-ui-palette-container .red-ui-palette-node").each(function(i,el) {
            var currentLabel = $(el).attr("data-palette-label");
            var type = $(el).attr("data-palette-type");
            if (val === "" || re.test(type) || re.test(currentLabel)) {
                $(el).attr("data-filter", null)
                $(this).show();
            } else {
                $(el).attr("data-filter", "true")
                $(this).hide();
            }
        });

        for (let category in categoryContainers) {
            if (categoryContainers.hasOwnProperty(category)) {
                const categorySection = categoryContainers[category]
                if (categorySection.getNodeCount(true) === 0) {
                    categorySection.hide()
                } else {
                    categorySection.show()
                    if (val) {
                        // There is a filter being applied and it has matched
                        // something in this category - show the contents
                        categorySection.open(true)
                    } else {
                        // No filter. Only show the category if it isn't in lastState
                        if (!paletteState.collapsed.includes(category)) {
                            categorySection.open(true)
                        } else if (categorySection.isOpen()) {
                            // This section should be collapsed but isn't - so make it so
                            categorySection.close(true, true)
                        }
                    }
                }
            }
        }
    }

    function init() {

        $('<img src="red/images/spin.svg" class="red-ui-palette-spinner hide"/>').appendTo("#red-ui-palette");
        $('<div id="red-ui-palette-search" class="red-ui-palette-search hide"><input type="text" data-i18n="[placeholder]palette.filter"></input></div>').appendTo("#red-ui-palette");
        $('<div id="red-ui-palette-container" class="red-ui-palette-scroll hide"></div>').appendTo("#red-ui-palette");
        $('<div class="red-ui-component-footer"></div>').appendTo("#red-ui-palette");
        $('<div id="red-ui-palette-shade" class="hide"></div>').appendTo("#red-ui-palette");

        $("#red-ui-palette > .red-ui-palette-spinner").show();

        RED.events.on('logout', function () {
            RED.settings.removeLocal('palette-state')
        })

        RED.events.on('registry:node-type-added', function(nodeType) {
            var def = RED.nodes.getType(nodeType);
            addNodeType(nodeType,def);
            if (def.onpaletteadd && typeof def.onpaletteadd === "function") {
                def.onpaletteadd.call(def);
            }
        });
        RED.events.on('registry:node-type-removed', function(nodeType) {
            removeNodeType(nodeType);
        });
        RED.events.on('registry:node-set-enabled', function(nodeSet) {
            for (var j=0;j<nodeSet.types.length;j++) {
                showNodeType(nodeSet.types[j]);
                var def = RED.nodes.getType(nodeSet.types[j]);
                if (def && def.onpaletteadd && typeof def.onpaletteadd === "function") {
                    def.onpaletteadd.call(def);
                }
            }
        });
        RED.events.on('registry:node-set-disabled', function(nodeSet) {
            for (var j=0;j<nodeSet.types.length;j++) {
                hideNodeType(nodeSet.types[j]);
                var def = RED.nodes.getType(nodeSet.types[j]);
                if (def && def.onpaletteremove && typeof def.onpaletteremove === "function") {
                    def.onpaletteremove.call(def);
                }
            }
        });
        RED.events.on('registry:node-set-removed', function(nodeSet) {
            if (nodeSet.added) {
                for (var j=0;j<nodeSet.types.length;j++) {
                    removeNodeType(nodeSet.types[j]);
                    var def = RED.nodes.getType(nodeSet.types[j]);
                    if (def && def.onpaletteremove && typeof def.onpaletteremove === "function") {
                        def.onpaletteremove.call(def);
                    }
                }
            }
        });

        RED.events.on("subflows:change",refreshSubflow);

        $("#red-ui-palette-search input").searchBox({
            delay: 100,
            change: function() {
                refreshFilter();
                paletteState.filter = $(this).val();
                savePaletteState();
            }
        });

        sidebarControls = $('<div class="red-ui-sidebar-control-left"><i class="fa fa-chevron-left"></i></div>').appendTo($("#red-ui-palette"));
        RED.popover.tooltip(sidebarControls,RED._("keyboard.togglePalette"),"core:toggle-palette");

        sidebarControls.on("click", function() {
            RED.menu.toggleSelected("menu-item-palette");
        })
        $("#red-ui-palette").on("mouseenter", function() {
            sidebarControls.toggle("slide", { direction: "left" }, 200);
        })
        $("#red-ui-palette").on("mouseleave", function() {
            sidebarControls.stop(false,true);
            sidebarControls.hide();
        })
        var userCategories = [];
        if (RED.settings.paletteCategories) {
            userCategories = RED.settings.paletteCategories;
        } else if (RED.settings.theme('palette.categories')) {
            userCategories = RED.settings.theme('palette.categories');
        }
        if (!Array.isArray(userCategories)) {
            userCategories = [];
        }

        var addedCategories = {};
        userCategories.forEach(function(category){
            addedCategories[category] = true;
            createCategoryContainer(category, escapeCategory(category), "palette.label."+escapeCategory(category));
        });
        coreCategories.forEach(function(category){
            if (!addedCategories[category]) {
                createCategoryContainer(category, escapeCategory(category), "palette.label."+escapeCategory(category));
            }
        });

        var paletteFooterButtons = $('<span class="button-group"></span>').appendTo("#red-ui-palette .red-ui-component-footer");
        var paletteCollapseAll = $('<button type="button" class="red-ui-footer-button"><i class="fa fa-angle-double-up"></i></button>').appendTo(paletteFooterButtons);
        paletteCollapseAll.on("click", function(e) {
            e.preventDefault();
            for (var cat in categoryContainers) {
                if (categoryContainers.hasOwnProperty(cat)) {
                    categoryContainers[cat].close();
                }
            }
        });
        RED.popover.tooltip(paletteCollapseAll,RED._('palette.actions.collapse-all'));

        var paletteExpandAll = $('<button type="button" class="red-ui-footer-button"><i class="fa fa-angle-double-down"></i></button>').appendTo(paletteFooterButtons);
        paletteExpandAll.on("click", function(e) {
            e.preventDefault();
            for (var cat in categoryContainers) {
                if (categoryContainers.hasOwnProperty(cat)) {
                    categoryContainers[cat].open();
                }
            }
        });
        RED.popover.tooltip(paletteExpandAll,RED._('palette.actions.expand-all'));

        RED.actions.add("core:toggle-palette", function(state) {
            if (state === undefined) {
                RED.menu.toggleSelected("menu-item-palette");
            } else {
                togglePalette(state);
            }
        });

        try {
            paletteState = JSON.parse(RED.settings.getLocal("palette-state") || '{"filter":"", "collapsed": []}');
            if (paletteState.filter) {
                // Apply the category filter
                $("#red-ui-palette-search input").searchBox("value", paletteState.filter);
            }
        } catch (error) {
            console.error("Unexpected error loading palette state from localStorage: ", error);
        }
        setTimeout(() => {
            // Lazily tidy up any categories that haven't been reloaded
            paletteState.collapsed = paletteState.collapsed.filter(category => !!categoryContainers[category])
            savePaletteState()
        }, 10000)
    }

    function togglePalette(state) {
        if (!state) {
            $("#red-ui-main-container").addClass("red-ui-palette-closed");
            sidebarControls.hide();
            sidebarControls.find("i").addClass("fa-chevron-right").removeClass("fa-chevron-left");
        } else {
            $("#red-ui-main-container").removeClass("red-ui-palette-closed");
            sidebarControls.find("i").removeClass("fa-chevron-right").addClass("fa-chevron-left");
        }
        setTimeout(function() { $(window).trigger("resize"); } ,200);
    }

    function getCategories() {
        var categories = [];
        $("#red-ui-palette-container .red-ui-palette-category").each(function(i,d) {
            categories.push({id:$(d).data('category'),label:$(d).data('label')});
        })
        return categories;
    }

    function savePaletteState() {
        try {
            RED.settings.setLocal("palette-state", JSON.stringify(paletteState));
        } catch (error) {
            console.error("Unexpected error saving palette state to localStorage: ", error);
        }
    }

    return {
        init: init,
        add:addNodeType,
        remove:removeNodeType,
        hide:hideNodeType,
        show:showNodeType,
        refresh:refreshNodeTypes,
        getCategories: getCategories
    };
})();
