    /*

    Siesta 5.3.0
    Copyright(c) 2009-2019 Bryntum AB
    https://bryntum.com/contact
    https://bryntum.com/products/siesta/license

    */
    Ext.define("Sch.data.mixin.FilterableTreeStore", {

        isFilteredFlag                      : false,
        isHiddenFlag                        : false,

        // ref to the last filter applied
        lastTreeFilter                      : null,
        lastTreeHiding                      : null,

        /**
         * @cfg {Boolean} allowExpandCollapseWhileFiltered When enabled (by default), tree store allows user to expand/collapse nodes while it is
         * filtered with the {@link #filterTreeBy} method. Please set it explicitly to `false` to restore the previous behavior,
         * where collapse/expand operations were disabled.
         */
        allowExpandCollapseWhileFiltered    : true,

        /**
         * @cfg {Boolean} reApplyFilterOnDataChange When enabled (by default), tree store will update the filtering (both {@link #filterTreeBy}
         * and {@link #hideNodesBy}) after new data is added to the tree or removed from it. Please set it explicitly to `false` to restore the previous behavior,
         * where this feature did not exist.
         */
        reApplyFilterOnDataChange           : true,

        suspendIncrementalFilterRefresh     : 0,

        filterGeneration                    : 0,
        currentFilterGeneration             : null,

        dataChangeListeners                 : null,
        monitoringDataChange                : false,

        filterer                            : null,

        onClassMixedIn : function (cls) {
            cls.override(Sch.data.mixin.FilterableTreeStore.prototype.inheritables() || {});
        },

        // Events (private)
        //    'filter-set',
        //    'filter-clear',
        //    'nodestore-datachange-start',
        //    'nodestore-datachange-end'

        /**
         * Should be called in the constructor of the consuming class, to activate the filtering functionality.
         */
        initTreeFiltering : function () {
            this.filterer   = new Siesta.Util.TreeStoreFilterer({
                isLeaf          : function (node) { return node.data.leaf },
                idProp          : 'internalId',
                childNodesProp  : 'childNodes',
                parentNodeProp  : 'parentNode'
            })

            this.treeFilter = new Ext.util.Filter({
                filterFn    : this.isNodeFilteredIn,
                scope       : this
            });

            this.dataChangeListeners    = {
                nodeappend  : this.onNeedToUpdateFilter,
                nodeinsert  : this.onNeedToUpdateFilter,

                scope       : this
            };
        },

        startDataChangeMonitoring : function () {
            if (this.monitoringDataChange) return;

            this.monitoringDataChange   = true;

            this.on(this.dataChangeListeners);
        },

        stopDataChangeMonitoring : function () {
            if (!this.monitoringDataChange) return;

            this.monitoringDataChange   = false;

            this.un(this.dataChangeListeners);
        },

        onNeedToUpdateFilter : function () {
            if (this.reApplyFilterOnDataChange && !this.suspendIncrementalFilterRefresh) this.reApplyFilter();
        },

        /**
         * Clears the current filter (if any).
         *
         * See also {@link Sch.data.mixin.FilterableTreeStore} for additional information.
         */
        clearTreeFilter : function () {
            if (!this.isTreeFiltered()) return;

            this.currentFilterGeneration = null;
            this.isFilteredFlag     = false;
            this.lastTreeFilter     = null;

            if (!this.isTreeFiltered(true)) this.stopDataChangeMonitoring();

            this.refreshNodeStoreContent();

            this.fireEvent('filter-clear', this);
        },

        reApplyFilter : function () {
            // bypass the nodeStore content refresh if store has both hiding and filtering
            if (this.isHiddenFlag) this.hideNodesBy.apply(this, this.lastTreeHiding.concat(this.isFilteredFlag));

            if (this.isFilteredFlag) this.filterTreeBy(this.lastTreeFilter);
        },

        refreshNodeStoreContent : function () {
            var me      = this,
                filters = me.getFilters();

            if (filters.indexOf(me.treeFilter) < 0) {
                me.addFilter(me.treeFilter);
            } else {
                this.getFilters().fireEvent('endupdate', this.getFilters());
            }
        },

        getIndexInTotalDataset : function (record) {
            var root            = this.getRootNode(),
                index           = -1;

            var rootVisible     = this.rootVisible;

            if (!rootVisible && record == root) return -1;

            var isFiltered      = this.isTreeFiltered();
            var currentFilterGeneration = this.currentFilterGeneration;

            var collectNodes    = function (node) {
                if (isFiltered && node.__filterGen != currentFilterGeneration || node.hidden)
                // stop scanning if record we are looking for is hidden
                    if (node == record) return false;

                if (rootVisible || node != root) index++;

                // stop scanning if we found the record
                if (node == record) return false;

                if (!node.data.leaf && node.isExpanded()) {
                    var childNodes  = node.childNodes,
                        length      = childNodes.length;

                    for (var k = 0; k < length; k++)
                        if (collectNodes(childNodes[ k ]) === false) return false;
                }
            };

            collectNodes(root);

            return index;
        },

        /**
         * Returns true if this store is currently filtered
         *
         * @return {Boolean}
         */
        isTreeFiltered : function (orHasHiddenNodes) {
            return this.isFilteredFlag || orHasHiddenNodes && this.isHiddenFlag;
        },

        markFilteredNodes : function (params) {
            var me                  = this;
            var filterGen           = this.currentFilterGeneration;

            var root                = this.getRootNode()

            var visibleNodes        = this.filterer.collectNodes(root, Ext.apply({
                rootVisible     : this.rootVisible
            }, params));

            root.cascadeBy(function (node) {
                if (visibleNodes[ node.internalId ]) {
                    node.__filterGen = filterGen;

                    if (me.allowExpandCollapseWhileFiltered && !node.data.leaf) node.expand();
                }
            });
        },

        /**
         * This method filters the tree store. It accepts an object with the following properties:
         *
         * - `filter` - a function to check if a node should be included in the result. It will be called for each **leaf** node in the tree and will receive the current node as the first argument.
         * It should return `true` if the node should remain visible, `false` otherwise. The result will also contain all parents nodes of all matching leafs. Results will not include
         * parent nodes, which do not have at least one matching child.
         * To call this method for parent nodes too, pass an additional parameter - `checkParents` (see below).
         * - `scope` - a scope to call the filter with (optional)
         * - `checkParents` - when set to `true` will also call the `filter` function for each parent node. If the function returns `false` for some parent node,
         * it could still be included in the filtered result if some of its children match the `filter` (see also "shallow" option below). If the function returns `true` for a parent node, it will be
         * included in the filtering results even if it does not have any matching child nodes.
         * - `shallow` - implies `checkParents`. When set to `true`, it will stop checking child nodes if the `filter` function return `false` for a parent node. The whole sub-tree, starting
         * from a non-matching parent, will be excluded from the result in such case.
         * - `onlyParents` - alternative to `checkParents`. When set to `true` it will only call the provided `filter` function for parent tasks. If
         * the filter returns `true`, the parent and all its direct child leaf nodes will be included in the results. If the `filter` returns `false`, a parent node still can
         * be included in the results (w/o direct children leafs), if some of its child nodes matches the filter.
         * - `fullMatchingParents` - implies `onlyParents`. In this mode, if a parent node matches the filter, then not only its direct children
         * will be included in the results, but the whole sub-tree, starting from the matching node.
         *
         * Repeated calls to this method will clear previous filters.
         *
         * This function can be also called with 2 arguments, which should be the `filter` function and `scope` in such case.
         *
         * For example:

         treeStore.filterTreeBy({
            filter          : function (node) { return node.get('name').match(/some regexp/) },
            checkParents    : true
        })

         // or, if you don't need to set any options:
         treeStore.filterTreeBy(function (node) { return node.get('name').match(/some regexp/) })

         *
         * See also {@link Sch.data.mixin.FilterableTreeStore} for additional information.
         *
         * @param {Object} params
         */
        filterTreeBy : function (params, scope) {
            this.currentFilterGeneration = this.filterGeneration++;

            var filter;

            if (arguments.length == 1 && Ext.isObject(arguments[ 0 ])) {
                scope       = params.scope;
                filter      = params.filter;
            } else {
                filter      = params;
                params      = { filter : filter, scope : scope };
            }

            this.fireEvent('nodestore-datachange-start', this);

            params                      = params || {};

            this.markFilteredNodes(params);

            this.startDataChangeMonitoring();

            this.isFilteredFlag     = true;
            this.lastTreeFilter     = params;

            this.refreshNodeStoreContent();

            this.fireEvent('nodestore-datachange-end', this);

            this.fireEvent('filter-set', this);
        },

        isNodeFilteredIn : function (node) {
            var isFiltered              = this.isTreeFiltered();
            var currentFilterGeneration = this.currentFilterGeneration;

            return this.loading || !Boolean(isFiltered && node.__filterGen != currentFilterGeneration || node.hidden);
        },

        hasNativeFilters : function () {
            var me      = this,
                filters = me.getFilters(),
                count   = filters.getCount();

            return (count && count > 1) || filters.indexOf(me.treeFilter) < 0;
        },

        /**
         * Hide nodes from the visual presentation of tree store (they still remain in the store).
         *
         * See also {@link Sch.data.mixin.FilterableTreeStore} for additional information.
         *
         * @param {Function} filter - A filtering function. Will be called for each node in the tree store and receive
         * the current node as the 1st argument. Should return `true` to **hide** the node
         * and `false`, to **keep it visible**.
         * @param {Object} scope (optional).
         */
        hideNodesBy : function (filter, scope, skipNodeStoreRefresh) {
            var me      = this;

            if (me.isFiltered() && me.hasNativeFilters()) throw new Error("Can't hide nodes of the filtered tree store");

            scope       = scope || me;

            me.getRootNode().cascadeBy(function (node) {
                node.hidden = Boolean(filter.call(scope, node, me));
            });

            me.startDataChangeMonitoring();

            me.isHiddenFlag     = true;
            me.lastTreeHiding   = [ filter, scope ];

            if (!skipNodeStoreRefresh) me.refreshNodeStoreContent();
        },

        /**
         * Shows all nodes that was previously hidden with {@link #hideNodesBy}
         *
         * See also {@link Sch.data.mixin.FilterableTreeStore} for additional information.
         */
        showAllNodes : function (skipNodeStoreRefresh) {
            this.getRootNode().cascadeBy(function (node) {
                node.hidden     = false;
            });

            this.isHiddenFlag       = false;
            this.lastTreeHiding     = null;

            if (!this.isTreeFiltered(true)) this.stopDataChangeMonitoring();

            if (!skipNodeStoreRefresh) this.refreshNodeStoreContent();
        },

        inheritables : function () {
            return {
                // @OVERRIDE
                onNodeExpand: function (parent, records, suppressEvent) {
                    if (this.isTreeFiltered(true) && parent == this.getRoot()) {
                        this.callParent(arguments);
                        // the expand of the root node - most probably its the data loading
                        this.reApplyFilter();
                    } else
                        return this.callParent(arguments);
                },

                // @OVERRIDE
                onNodeCollapse: function (parent, records, suppressEvent, callback, scope) {
                    var me                      = this;
                    var data                    = me.data;
                    var prevContains            = data.contains;

                    var isFiltered              = me.isTreeFiltered();
                    var currentFilterGeneration = me.currentFilterGeneration;

                    // the default implementation of `onNodeCollapse` only checks if the 1st record from collapsed nodes
                    // exists in the node store. Meanwhile, that 1st node can be hidden, so we need to check all of them
                    // thats what we do in the `for` loop below
                    // then, if we found a node, we want to do actual removing of nodes and we override the original code from NodeStore
                    // by always returning `false` from our `data.contains` override
                    data.contains           = function () {
                        var node, sibling, lastNodeIndexPlus;

                        var collapseIndex   = me.indexOf(parent) + 1;
                        var found           = false;

                        for (var i = 0; i < records.length; i++)
                            if (
                                !(records[ i ].hidden || isFiltered && records[ i ].__filterGen != currentFilterGeneration) &&
                                prevContains.call(this, records[ i ])
                            ) {
                                // this is our override for internal part of `onNodeCollapse` method

                                // Calculate the index *one beyond* the last node we are going to remove
                                // Need to loop up the tree to find the nearest view sibling, since it could
                                // exist at some level above the current node.
                                node = parent;
                                while (node.parentNode) {
                                    sibling = node;
                                    do {
                                        sibling = sibling.nextSibling;
                                    } while (sibling && (sibling.hidden || isFiltered && sibling.__filterGen != currentFilterGeneration));

                                    if (sibling) {
                                        found = true;
                                        lastNodeIndexPlus = me.indexOf(sibling);
                                        break;
                                    } else {
                                        node = node.parentNode;
                                    }
                                }
                                if (!found) {
                                    lastNodeIndexPlus = me.getCount();
                                }

                                // Remove the whole collapsed node set.
                                me.removeAt(collapseIndex, lastNodeIndexPlus - collapseIndex);

                                break;
                            }

                        // always return `false`, so original NodeStore code won't execute
                        return false;
                    };

                    this.callParent(arguments);

                    data.contains           = prevContains;
                },

                // @OVERRIDE
                handleNodeExpand : function (parent, records, toAdd) {
                    var me                      = this;
                    var visibleRecords          = [];
                    var isFiltered              = me.isTreeFiltered();
                    var currentFilterGeneration = me.currentFilterGeneration;

                    for (var i = 0; i < records.length; i++) {
                        var record          = records[ i ];

                        if (
                            !(isFiltered && record.__filterGen != currentFilterGeneration || record.hidden)
                        ) {
                            visibleRecords[ visibleRecords.length ] = record;
                        }
                    }

                    return this.callParent([ parent, visibleRecords, toAdd ]);
                },

                // @OVERRIDE
                onNodeInsert: function(parent, node, index) {
                    var me = this,
                        refNode,
                        sibling,
                        storeReader,
                        nodeProxy,
                        nodeReader,
                        reader,
                        data = node.raw || node.data,
                        dataRoot,
                        isVisible,
                        childType;

                    if (me.filterFn) {
                        isVisible = me.filterFn(node);
                        node.set('visible', isVisible);

                        // If a node which passes the filter is added to a parent node
                        if (isVisible) {
                            parent.set('visible', me.filterFn(parent));
                        }
                    }

                    // Register node by its IDs
                    me.registerNode(node, true);

                    me.beginUpdate();

                    // Only react to a node append if it is to a node which is expanded.
                    if (me.isVisible(node)) {
                        if (index === 0 || !node.previousSibling) {
                            refNode = parent;
                        } else {
                            // Find the previous visible sibling (filtering may have knocked out intervening nodes)
                            for (sibling = node.previousSibling; sibling && !sibling.get('visible'); sibling = sibling.previousSibling);
                            if (!sibling) {
                                refNode = parent;
                            } else {
                                while (sibling.isExpanded() && sibling.lastChild) {
                                    sibling = sibling.lastChild;
                                }
                                refNode = sibling;
                            }
                        }

                        // The reaction to collection add joins the node to this Store
                        me.insert(me.indexOf(refNode) + 1, node);
                        if (!node.isLeaf() && node.isExpanded()) {
                            if (node.isLoaded()) {
                                // Take a shortcut
                                me.onNodeExpand(node, node.childNodes);
                            } else if (!me.fillCount) {
                                // If the node has been marked as expanded, it means the children
                                // should be provided as part of the raw data. If we're filling the nodes,
                                // the children may not have been loaded yet, so only do this if we're
                                // not in the middle of populating the nodes.
                                node.set('expanded', false);
                                node.expand();
                            }
                        }
                    }

                    // Set sync flag if the record needs syncing.
                    else {
                        me.needsSync = me.needsSync || node.phantom || node.dirty;
                    }

                    if (!node.isLeaf() && !node.isLoaded() && !me.lazyFill) {
                        // With heterogeneous nodes, different levels may require differently configured readers to extract children.
                        // For example a "Disk" node type may configure its proxy reader with root: 'folders', while a "Folder" node type
                        // might configure its proxy reader with root: 'files'. Or the root property could be a configured-in accessor.
                        storeReader = me.getProxy().getReader();
                        nodeProxy = node.getProxy();
                        nodeReader = nodeProxy ? nodeProxy.getReader() : null;

                        // If the node's reader was configured with a special root (property name which defines the children array) use that.
                        reader = nodeReader && nodeReader.initialConfig.rootProperty ? nodeReader : storeReader;

                        dataRoot = reader.getRoot(data);
                        if (dataRoot) {
                            childType = node.childType;
                            me.fillNode(node, reader.extractData(dataRoot, childType ? {
                                model: childType
                            } : undefined));
                        }
                    }
                    me.endUpdate();
                },

                isFiltered : function () {
                    return this.callParent(arguments) || this.isTreeFiltered();
                }
            };
        }

    });