/** @module AppTree */
import $ from "jquery";
import { Observer, inject, observer } from "mobx-react";
import { Component } from "react";

import {
    Alert,
    Icon,
    Popconfirm,
    Tree,
    getRecursivelyCheckedNodes,
    getTreeParentById,
    immer,
    message,
} from "@devowl-wp/react-folder-tree";
import { RatingPointer, isRatable } from "@devowl-wp/real-utils";

import { DashIcon } from ".//index.js";
import MetaBox from "../components/MetaBox.js";
import { ProBox, ProFooter } from "../components/ProFooter.js";
import createLockedToolTipText from "../hooks/permissions.js";
import { orderUrl, toggleSortable } from "../hooks/sortable.js";
import { ID_NONE, getDefaultFolder } from "../others/defaultFolder.js";
import { FILTER_SELECTOR } from "../others/filter.js";
import renderOrderMenu from "../others/renderOrderMenu.js";
import renderSortMenu, { RearrangeBox } from "../others/renderSortMenu.js";
import { draggable, droppable } from "../util/dragdrop.js";
import {
    ICON_OBJ_FOLDER_CLOSED,
    ICON_OBJ_FOLDER_COLLECTION,
    ICON_OBJ_FOLDER_GALLERY,
    ICON_OBJ_FOLDER_OPEN,
    addUrlParam,
    hooks,
    i18n,
    isMaterialWp,
    materialWpResizeOpposite,
    request,
    resolveIcon,
    rmlOpts,
    urlParam,
} from "../util/index.js";

/**
 * The latest queried folder.
 *
 * @deprecated Do no longer use it, use rmlOpts.others.lastQueried instead
 * @type object
 */
export let latestQueriedFolder = { node: null };

message.config({ top: 50 });

/**
 * The application tree handler for Real Media Library.
 *
 * @param {string} id The HTML id (needed to localStorage support)
 * @param {object} [attachmentsBrowser] The attachments browser (for media grid view)
 * @param {boolean} [isModal=false] If true the given app tree is a modal dialog
 * @param {module:AppTree~AppTree~init} [init]
 * @see module:store.StoredAppTree
 * @see module:@devowl-wp/react-folder-tree~Tree
 * @extends Component
 */
@inject("store")
@observer
class AppTree extends Component {
    /**
     * Initialize properties and state for AIOTree component.
     * Also handles the responsiveness.
     */
    constructor(props) {
        super(props);

        // Add respnsive handler for non-modal views
        !props.isModal && $(window).resize(this.handleWindowResize);
        const isMobile = this._isMobile();

        // State refs (see https://github.com/reactjs/redux/issues/1793) and #resolveStateRefs
        this.stateRefs = {
            keysCreatable: "icon,iconActive,toolTipTitle,toolTipText,onClick,label".split(","),
            keysToolbar: "content,toolTipTitle,toolTipText,onClick,onCancel,onSave,modifier,label,save,menu".split(","),

            // Icons
            ICON_OBJ_FOLDER_CLOSED,
            ICON_OBJ_FOLDER_OPEN,
            ICON_OBJ_FOLDER_COLLECTION,
            ICON_OBJ_FOLDER_GALLERY,
            ICON_SETTINGS: <Icon type="setting" />,
            ICON_LOCKED: <Icon type="lock" />,
            ICON_ORDER: <DashIcon name="move" />,
            ICON_RELOAD: <Icon type="reload" />,
            ICON_RENAME: <Icon type="edit" />,
            ICON_TRASH: <Icon type="delete" />,
            ICON_SORT: <DashIcon name="sort" />,
            ICON_SAVE: <Icon type="save" />,
            ICON_ELLIPSIS: <Icon type="ellipsis" />,

            // Creatable
            handleCreatableClickBackButton: () => this.handleCreatableClick(),
            handleCreatableClickFolder: () => this.handleCreatableClick("folder", 0),
            handleCreatableClickCollection: rmlOpts.others.isPro
                ? () => this.handleCreatableClick("collection", 1)
                : () => this.setState({ showProFeature: "collections" }),
            handleCreatableClickGallery: () => this.handleCreatableClick("gallery", 2),

            // Toolbar buttons
            renderOrderMenu: renderOrderMenu.bind(this),
            renderSortMenu: renderSortMenu.bind(this),
            handleOrderClick: this.handleOrderClick,
            handleOrderCancel: this.handleOrderCancel,
            handleReload: this.handleReload,
            handleRenameClick: this.handleRenameClick,
            handleRenameCancel: this.handleRenameCancel,
            handleTrashModifier: (body) => {
                const node = this.getTreeItemById();
                return node ? (
                    <Popconfirm
                        placement="bottom"
                        onConfirm={this.handleTrash}
                        title={i18n("deleteConfirm", { name: node.title }, "maxWidth")}
                        okText={i18n("ok")}
                        cancelText={i18n("cancel")}
                    >
                        {body}
                    </Popconfirm>
                ) : (
                    body
                );
            },
            handleMultipleTrashModifier: (body) => {
                const checkedNodes = getRecursivelyCheckedNodes(this.props.store.tree);
                return (
                    <Popconfirm
                        placement="bottom"
                        onConfirm={this.handleMultipleTrash}
                        title={i18n("deleteMultipleConfirm", { count: checkedNodes.length }, "maxWidth")}
                        okText={i18n("ok")}
                        cancelText={i18n("cancel")}
                    >
                        {body}
                    </Popconfirm>
                );
            },
            handleSortClick: () => this._handleSortNode("sort"),
            handleSortCancel: () => this._handleSortNode(),
            handleDetailsClick: () => this._handleDetails("details"),
            handleUserSettingsClick: () => this._handleDetails("usersettings"),
        };

        // Determine selected id and fetch tree
        let selectedId = getDefaultFolder();

        this.attachmentsBrowser = props.attachmentsBrowser;
        this.state = {
            // Custom
            currentFolderRestrictions: [],
            isModal: props.isModal,
            isMoveable: true,
            isWPAttachmentsSortMode: false, // See modal.js
            initialSelectedId: !selectedId || selectedId === "all" ? "all" : +selectedId,
            metaBoxId: false,
            rearrangeBoxId: false,
            showProFeature: undefined,

            // Creatables
            availableCreatables: (rmlOpts.others.isPro || rmlOpts.others.showProHints
                ? "folder,collection,gallery"
                : "folder"
            ).split(","),
            selectedCreatableType: undefined, // The selected folder type
            creatable_folder: {
                icon: "ICON_OBJ_FOLDER_CLOSED",
                iconActive: "ICON_OBJ_FOLDER_OPEN",
                visibleInFolderType: [undefined, 0],
                cssClasses: "page-title-action add-new-h2",
                toolTipTitle: "i18n.creatable0ToolTipTitle",
                toolTipText: "i18n.creatable0ToolTipText",
                label: "+",
                onClick: "handleCreatableClickFolder",
            },
            creatable_collection: {
                icon: "ICON_OBJ_FOLDER_COLLECTION",
                visibleInFolderType: [undefined, 0, 1],
                cssClasses: "page-title-action add-new-h2",
                toolTipTitle: "i18n.creatable1ToolTipTitle",
                toolTipText: "i18n.creatable1ToolTipText",
                label: "+",
                onClick: "handleCreatableClickCollection",
            },
            creatable_gallery: {
                icon: "ICON_OBJ_FOLDER_GALLERY",
                visibleInFolderType: [1],
                visible: false,
                cssClasses: "page-title-action add-new-h2",
                toolTipTitle: "i18n.creatable2ToolTipTitle",
                toolTipText: "i18n.creatable2ToolTipText",
                label: "+",
                onClick: "handleCreatableClickGallery",
            },
            creatableBackButton: {
                cssClasses: "page-title-action add-new-h2",
                label: "i18n.cancel",
                onClick: "handleCreatableClickBackButton",
            },

            // Toolbar buttons
            availableToolbarButtons: (rmlOpts.others.isPro || rmlOpts.others.showProHints
                ? "locked,usersettings,order,reload,rename,trash,sort,details"
                : "locked,usersettings,reload,rename,trash,sort,details"
            ).split(","),
            toolbar_usersettings: {
                content: "ICON_SETTINGS",
                visible: !!+rmlOpts.others.userSettings,
                toolTipTitle: "i18n.userSettingsToolTipTitle",
                toolTipText: "i18n.userSettingsToolTipText",
                onClick: "handleUserSettingsClick",
            },
            toolbar_locked: {
                content: "ICON_LOCKED",
                visible: false,
                toolTipTitle: "i18n.lockedToolTipTitle",
                toolTipText: "", // Lazy
            },
            toolbar_order: {
                content: "ICON_ORDER",
                toolTipTitle: "i18n.orderToolTipTitle",
                toolTipText: "i18n.orderToolTipText",
                onClick: "handleOrderClick",
                onCancel: "handleOrderCancel",
                menu: "resolve.renderOrderMenu",
                toolTipPlacement: "topLeft",
                dropdownPlacement: "bottomLeft",
            },
            toolbar_reload: {
                content: "ICON_RELOAD",
                toolTipTitle: "i18n.refreshToolTipTitle",
                toolTipText: "i18n.refreshToolTipText",
                onClick: "handleReload",
            },
            toolbar_rename: {
                content: "ICON_RENAME",
                toolTipTitle: "i18n.renameToolTipTitle",
                toolTipText: "i18n.renameToolTipText",
                onClick: "handleRenameClick",
                onCancel: "handleRenameCancel",
                disabled: true,
            },
            toolbar_trash: {
                content: "ICON_TRASH",
                toolTipTitle: "i18n.trashToolTipTitle",
                toolTipText: "i18n.trashToolTipText",
                modifier: "handleTrashModifier",
                disabled: true,
            },
            toolbar_sort: {
                content: "ICON_SORT",
                toolTipTitle: "i18n.sortToolTipTitle",
                toolTipText: "i18n.sortToolTipText",
                onClick: "handleSortClick",
                onCancel: "handleSortCancel",
                menu: "resolve.renderSortMenu",
                toolTipPlacement: "topLeft",
                dropdownPlacement: "bottomLeft",
            },
            toolbar_details: {
                content: "ICON_ELLIPSIS",
                disabled: true,
                toolTipTitle: "i18n.detailsToolTipTitle",
                toolTipText: "i18n.detailsToolTipText",
                onClick: "handleDetailsClick",
            },
            toolbarBackButton: {
                label: "i18n.cancel",
                save: "i18n.save",
            },

            // Multitoolbar buttons
            availableMultiToolbarButtons: "trash".split(","),
            multiToolbar_trash: {
                content: "ICON_TRASH",
                toolTipTitle: "i18n.trashToolTipTitle",
                toolTipText: "i18n.trashMultipleToolTipText",
                modifier: "handleMultipleTrashModifier",
            },

            // AIO
            isResizable: !isMobile,
            isSticky: !isMobile,
            isStickyHeader: !isMobile,
            isFullWidth: isMobile,
            style: isMobile ? { marginLeft: 10 } : {},
            isSortable: true,
            isSortableDisabled: true,
            isTreeBusy: false,
            isBusyHeader: false,
            headerStickyAttr: {
                top: "#wpadminbar",
            },
            isCreatableLinkDisabled: false,
            toolbarActiveButton: undefined,
            isTreeLinkDisabled: false,
            onResizeOpposite: isMaterialWp() && materialWpResizeOpposite,
        };

        // What happens if the attachments browser is available? We will add a reference to this React element
        this.attachmentsBrowser && (this.attachmentsBrowser.controller.$RmlAppTree = this);

        /**
         * Called on initialzation and allows you to modify the init state.
         *
         * @callback module:AppTree~AppTree~init
         * @param {object} state The default state
         * @param {AppTree} tree The AppTree component instance
         * @returns {object} The new state
         */
        props.init && (this.state = props.init(this.state, this));

        /**
         * The React AppTree instance gets constructed and you can modify it here.
         *
         * @event module:util/hooks#tree/init
         * @param {object} state
         * @param {object} props
         * @this module:AppTree~AppTree
         */
        hooks.call("tree/init", [this.state, props], this);
        this.initialSelectedId = this.state.initialSelectedId;
    }

    /**
     * Render AIO tree with tax switcher.
     */
    render() {
        //{ metaBoxId !== false && (<MetaBox treeInstance={ this } patcher={ patcher => (this.metaboxPatcher = patcher) }
        // busy={ isBusyHeader } errors={ metaBoxErrors } id={ metaBoxId } />) }

        const { staticTree, tree } = this.props.store;
        const { metaBoxId, rearrangeBoxId, showProFeature } = this.state;
        return (
            <Tree
                ref={this.doRef}
                id={this.props.id}
                rootId={+rmlOpts.others.rootId}
                staticTree={staticTree}
                tree={tree.length > 0 ? tree : []}
                opposite={document.getElementById("wpbody-content")}
                onSelect={this.handleSelect}
                onCloseMultiToolbar={this.handleCloseMultiToolbar}
                onRenameClose={this.handleRenameClose}
                onAddClose={this.handleAddClose}
                onCheck={this.handleCheck}
                onNodeExpand={this.handleDelayedDroppable}
                onSearchResult={this.handleDelayedDroppable}
                renderItem={this.onTreeNodeRender}
                renderIcon={this.onTreeNodeRenderIcon}
                onNodePressF2={this.handleRenameClick}
                onSort={this.handleSort}
                onResize={this.handleResize}
                headline={<span style={{ paddingRight: 5 }}>{i18n("folders")}</span>}
                renameSaveText={this.stateRefs.ICON_SAVE}
                renameAddText={this.stateRefs.ICON_SAVE}
                noFoldersTitle={i18n("noFoldersTitle")}
                noFoldersDescription={i18n("noFoldersDescription")}
                noSearchResult={i18n("noSearchResult")}
                innerClassName="wrap"
                theme="wordpress"
                creatable={this.renderCreatables()}
                toolbar={this.renderToolbarButtons()}
                multiToolbar={this.renderToolbarButtons(true)}
                forceSortableFallback
                allowMultiSelect
                {...this.state}
                // Sortable
                sortableDelay={this.state.isSortableDisabled ? 150 : 0}
                isSortableDisabled={false}
            >
                {rmlOpts.others.isDevLicense && (
                    <Alert
                        message={
                            <>
                                {i18n("licenseIsDev")} (
                                <a href={rmlOpts.others.lang.devLicenseLink} rel="noreferrer" target="_blank">
                                    {i18n("devLicenseLearnMore")}
                                </a>
                                )
                            </>
                        }
                        type="warning"
                        style={{ marginBottom: "10px" }}
                    />
                )}

                {rmlOpts.others.showTaxImportNotice && (
                    <Alert
                        message={
                            <span>
                                {rmlOpts.others.lang.sidebarDetectedTax}{" "}
                                <a href={rmlOpts.others.taxImportNoticeLink}>
                                    {rmlOpts.others.lang.sidebarDetectedTaxImport}
                                </a>{" "}
                                &middot;{" "}
                                <a href="#" onClick={this.handleDismissImportTaxNotice}>
                                    {rmlOpts.others.lang.sidebarDetectedTaxDismiss}
                                </a>
                            </span>
                        }
                        type="info"
                        style={{ marginBottom: "10px" }}
                    />
                )}

                {!rmlOpts.others.isPro && rmlOpts.others.showProHints && rmlOpts.others.showLiteNotice && (
                    <ProFooter dismissible feature="sidebar" />
                )}

                <ProBox feature={showProFeature} onClose={() => this.setState({ showProFeature: undefined })} />
                <MetaBox
                    id={metaBoxId}
                    onClose={(status, response) => this._handleDetails(undefined, status, response)}
                />
                <RearrangeBox
                    id={rearrangeBoxId}
                    onClose={() => this.setState({ rearrangeBoxId: undefined })}
                    onSort={this.handleSortManual}
                />
            </Tree>
        );
    }

    /**
     * @returns {object}
     */
    renderToolbarButtons = (multi) => {
        let availableToolbarButtons = multi
            ? this.state.availableMultiToolbarButtons
            : this.state.availableToolbarButtons;
        let { toolbarBackButton } = this.state;

        const toolbar = {
            buttons: {},
            backButton: this.resolveStateRefs(toolbarBackButton, "keysToolbar"),
        };
        for (let i = 0; i < availableToolbarButtons.length; i++) {
            toolbar.buttons[availableToolbarButtons[i]] = this.resolveStateRefs(
                this.state[(multi ? "multiToolbar_" : "toolbar_") + availableToolbarButtons[i]],
                "keysToolbar",
            );
        }
        return toolbar;
    };

    /**
     * @returns {object}
     */
    renderCreatables = () => {
        const { availableCreatables, creatableBackButton } = this.state;
        const creatable = {
            buttons: {},
            backButton: this.resolveStateRefs(creatableBackButton, "keysCreatable"),
        };
        for (let i = 0; i < availableCreatables.length; i++) {
            creatable.buttons[availableCreatables[i]] = this.resolveStateRefs(
                this.state[`creatable_${availableCreatables[i]}`],
                "keysCreatable",
            );
        }
        return creatable;
    };

    /**
     * Iterates all available values in an object and resolve it with the available
     * this::stateRefs.
     *
     * @returns {object}
     */
    resolveStateRefs(_obj, keys) {
        const obj = Object.assign({}, _obj);
        let value;
        let newValue;
        for (let key in obj) {
            if (
                Object.prototype.hasOwnProperty.call(obj, key) &&
                (value = obj[key]) &&
                this.stateRefs[keys].indexOf(key) > -1 &&
                typeof value === "string" &&
                (newValue = this.resolveStateRef(value))
            ) {
                obj[key] = newValue;
            }
        }
        return obj;
    }

    /**
     * Resolve single state ref key.
     *
     * @returns {object}
     */
    resolveStateRef(key) {
        if (typeof key !== "string") {
            return;
        }
        if (key.indexOf("i18n.") === 0) {
            return i18n(key.substr(5));
        } else if (key.indexOf("resolve.") === 0) {
            return this.stateRefs[key.substr(8)]();
        } else if (this.stateRefs[key]) {
            return this.stateRefs[key];
        }
    }

    doRef = (ref) => (this.ref = ref);

    /**
     * Remove resize handler.
     */
    componentWillUnmount() {
        $(window).off("resize", this.handleWindowResize);

        /**
         * The React AppTree instance gets unmounted.
         *
         * @event module:util/hooks#tree/destroy
         * @param {object} state
         * @param {object} props
         * @this module:AppTree~AppTree
         */
        hooks.call("tree/destroy", [this.state, this.props], this);
    }

    /**
     * Initiate draggable and droppable
     */
    componentDidMount() {
        // Fetch initial tree
        this.fetchTree(this.initialSelectedId);

        draggable(this);
        droppable(this);
        this.handleResize();

        // If order should be enabled in list mode, then activate it now
        if (rmlOpts.others.listMode === "list" && window.location.hash === "#order") {
            this.handleOrderClick();
            window.location.hash = "";
        }
    }

    /**
     * When the component updates the droppable zone is reinitialized.
     * Also the toolbar buttons gets disabled or enabled depending on selected node.
     */
    componentDidUpdate() {
        const { selectedCreatableType } = this.state;
        const selected = this.getTreeItemById();
        if (
            (selected && selectedCreatableType !== selected.properties.type) ||
            (!selected && selectedCreatableType !== undefined)
        ) {
            this._updateCreatableButtons(selected ? selected.properties.type : undefined);
        }

        // Enable / Disable toolbar buttons
        this._updateToolbarButtons();

        // Enable locked toolbar item
        createLockedToolTipText(this);

        draggable(this);
        droppable(this);
    }

    /**
     * Return the backbone filter view for the given attachments browser.
     *
     * @returns object
     */
    getBackboneFilter() {
        const { attachmentsBrowser } = this;
        return attachmentsBrowser && attachmentsBrowser.toolbar.get("rml_folder");
    }

    /**
     * Get the selected node id.
     *
     * @returns {string|int}
     */
    getSelectedId() {
        return this.props.store.selectedId;
    }

    /**
     * Get tree item by id.
     *
     * @param {string|int} [id=Current]
     * @param {boolean} [excludeStatic=true]
     * @returns {object} Tree node
     */
    getTreeItemById(id = this.getSelectedId(), excludeStatic = true) {
        return this.props.store.getTreeItemById(id, excludeStatic);
    }

    /**
     * Update a tree item by id.
     *
     * @param {function|array} callback The callback with one argument (node draft) and should return the new node.
     * @param {string|int} [id=Current] The id which should be updated
     * @param {boolean} [setHash] If true the hash node is changed so a rerender is forced
     */
    updateTreeItemById(callback, id = this.getSelectedId(), setHash = false) {
        const node = this.props.store.getTreeItemById(id);
        node && node.setter(callback, setHash);
    }

    /**
     * Updates the create node. That's the node without id and the input field.
     *
     * @param {object} modifier The modifier object which is passed through Object.assign
     */
    async updateCreateNode(modifier) {
        // Root update
        const { createRoot } = this.state;
        createRoot &&
            this.setState({
                createRoot: immer.produce(createRoot, modifier),
            });

        // Child node update
        const node = this.getTreeItemById();
        node &&
            node.$create &&
            this.updateTreeItemById(
                (node) => {
                    const obj = { ...node.$create };
                    modifier(obj);
                    node.$create = obj;
                },
                undefined,
                true,
            );
    }

    /**
     * Disable the checked property for all checked nodes.
     *
     * @method
     */
    handleCloseMultiToolbar = (checkedNodes) => {
        checkedNodes.forEach((n) => n.toggleChecked(false, false));
    };

    /**
     * Handles the creatable click and creates a new node depending on the selected one.
     *
     * @method
     */
    handleCreatableClick = (type, typeInt) => {
        this._lastHandleCreatableClickArgs = [type, typeInt]; // @see handleAddClose
        let createRoot = undefined;
        let $create = undefined;

        if (type) {
            // Activate create
            const creatable = this.state[`creatable_${type}`];
            const newNode = {
                $rename: true,
                icon: this.resolveStateRef(creatable.icon),
                iconActive: this.resolveStateRef(creatable.iconActive),
                parent: +rmlOpts.others.rootId,
                typeInt,
            };
            const selectedId = this.getSelectedId();
            if (typeof selectedId !== "number" || [+rmlOpts.others.rootId, ID_NONE].indexOf(selectedId) > -1) {
                createRoot = newNode;
            } else {
                $create = newNode;
                newNode.parent = selectedId;
            }
        }

        this.setState({
            isTreeLinkDisabled: !!type,
            isCreatableLinkCancel: !!type,
            isToolbarActive: !type,
            createRoot,
        });
        this.updateTreeItemById((node) => {
            node.$create = $create;
        });
    };

    handleDelayedDroppable = () => {
        clearTimeout(this.timeout);
        this.timeout = setTimeout(() => droppable(this), 200);
    };

    handleCheck = (id) => {
        this.props.store.getTreeItemById(id).toggleChecked();
    };

    /**
     * A node gets selected. Depending on the fast mode the page gets reloaded
     * or the wp list table gets reloaded.
     *
     * @method
     */
    handleSelect = (id) => {
        // Do nothing when sort mode is active
        if (this.state.toolbarActiveButton === "sort") {
            return;
        }

        const select = this.getTreeItemById(id, false);
        const setter = (_id, $busy) => {
            latestQueriedFolder.node = select;
            latestQueriedFolder.node.setter((node) => {
                node.$busy = $busy;
                node.selected = true;
            });
            rmlOpts.others.lastQueried = select.id;

            /**
             * The user is selecting a node in the app tree.
             *
             * @event module:util/hooks#tree/select
             * @param {int|string} id
             * @param {object} select The MST node
             * @param {object} attachmentsBrowser
             * @this module:AppTree~AppTree
             * @since 4.0.5
             */
            hooks.call("tree/select", [_id, select, this.attachmentsBrowser], this);
        };

        if (this.attachmentsBrowser) {
            !id && this.attachmentsBrowser.collection.props.set({ ignore: +new Date() }); // Reload the view
            this._handleBackboneFilterSelection(select.id);
        } else {
            const keepParams = [
                {
                    param: "page",
                    value: urlParam("page"),
                },
                {
                    param: "paged",
                    value: urlParam("paged") !== null ? 1 : null,
                },
            ].filter(({ value }) => value !== null);

            let { href } = window.location;
            urlParam("orderby") === "rml" && ([href] = href.split("?"));

            for (const { param, value } of keepParams) {
                href = addUrlParam(href, param, value);
            }

            select.properties &&
                (select.contentCustomOrder === 1 || select.forceCustomOrder) &&
                (href = orderUrl(href));
            window.location.href = addUrlParam(href, "rml_folder", select.id);
        }
        setter(select.id, !this.attachmentsBrowser);
    };

    /**
     * When resizing the container set ideal width for attachments.
     */
    handleResize = () => {
        const { attachmentsBrowser } = this;
        attachmentsBrowser?.attachments?.setColumns();
    };

    /**
     * Handle order click.
     *
     * @method
     */
    handleOrderClick = () => {
        if (!rmlOpts.others.isPro && rmlOpts.others.showProHints) {
            this.setState({ showProFeature: "order-content" });
        } else {
            if (toggleSortable(this.getTreeItemById(), true, this.attachmentsBrowser)) {
                this.setState({
                    isMoveable: false,
                    toolbarActiveButton: "order",
                    toolbarBackButton: Object.assign(this.state.toolbarBackButton, {
                        label: "i18n.back",
                    }),
                });
            }
        }
    };

    /**
     * Handle order cancel.
     *
     * @method
     */
    handleOrderCancel = () => {
        toggleSortable(this.getTreeItemById(), false, this.attachmentsBrowser);
        this.setState({
            isMoveable: true,
            toolbarActiveButton: undefined,
            toolbarBackButton: Object.assign(this.state.toolbarBackButton, {
                label: "i18n.cancel",
            }),
        });
    };

    /**
     * Handle rename click and enable the input field if necessery.
     *
     * @method
     */
    handleRenameClick = () => this._handleRenameNode("rename", true, true, true);

    /**
     * Handle rename cancel.
     *
     * @method
     */
    handleRenameCancel = () => this._handleRenameNode(undefined, false, false, undefined);

    /**
     * Handle rename close and depending on the save state create the new node.
     *
     * @method
     */
    handleRenameClose = async (save, inputValue, { id, title }) => {
        if (save && inputValue.length && title !== inputValue) {
            const hide = message.loading(i18n("renameLoadingText", { name: inputValue }));
            try {
                const node = this.props.store.getTreeItemById(id);
                const { name } = await node.setName(inputValue);

                /**
                 * Folder successfully renamed.
                 *
                 * @event module:util/hooks#folder/renamed
                 * @param {module:store/TreeNode~TreeNode} node The node
                 * @this module:AppTree~AppTree
                 * @since 4.0.7
                 */
                hooks.call("folder/renamed", [node], this);

                message.success(i18n("renameSuccess", { name }));
                this.handleRenameCancel();
            } catch (e) {
                message.error(e.responseJSON.message);
            } finally {
                hide();
            }
        } else {
            this.handleRenameCancel();
        }
    };

    /**
     * (Pro only) Handle add close and remove the new node.
     *
     * @method
     */
    handleAddClose = async (save, name, { parent, typeInt }) => {
        if (save) {
            if (process.env.PLUGIN_CTX === "lite" && parent !== this.props.store.rootId) {
                this.setState({ showProFeature: "subfolder" });
                return;
            }
            this.updateCreateNode((obj) => {
                obj.$busy = true;
            });
            const hide = message.loading(i18n("addLoadingText", { name }));
            const ctrlHolding = $("body").hasClass("aiot-helper-ctrl");

            try {
                const newObj = await this.props.store.persist(name, { parent, typeInt }, () => {
                    if (ctrlHolding) {
                        // Allow bulk insert while holding ctrl + enter
                        this.handleCreatableClick(...this._lastHandleCreatableClickArgs);
                    } else {
                        this.handleCreatableClick();
                    }
                });

                // Show rating pointer
                isRatable(rmlOpts.slug) &&
                    this.ref &&
                    new RatingPointer(rmlOpts.slug, $(this.ref.container).find(".aiot-tree-headline"));

                message.success(i18n("addSuccess", { name }));

                // Modify all available attachments browsers filter
                let backboneFilter;
                let lastSlugs;
                $(FILTER_SELECTOR).each(function () {
                    backboneFilter = $(this).data("backboneView");
                    if (backboneFilter) {
                        ({ lastSlugs } = backboneFilter);
                        lastSlugs.names.push(`(NEW) ${name}`);
                        lastSlugs.slugs.push(newObj.id);
                        lastSlugs.types.push(typeInt);
                        backboneFilter.createFilters(lastSlugs);
                    }
                });

                !ctrlHolding && droppable(this);
            } catch (e) {
                message.error(e.responseJSON.message);
                this.updateCreateNode((obj) => {
                    obj.$busy = false;
                });
            } finally {
                hide();
            }
        } else {
            this.handleCreatableClick();
        }
    };

    /**
     * Handle trashing of a category. If the category has subcategories the
     * trash is forbidden.
     *
     * @method
     */
    handleTrash = async (e, node = this.getTreeItemById(), isMulti) => {
        // Check if subdirectories
        if (node.childNodes.filter((node) => node.$visible).length) {
            message.error(i18n("deleteFailedSub", { name: node.title }));
            return false;
        }

        const hide = message.loading(i18n("deleteLoadingText", { name: node.title }));
        try {
            await node.trash();
            !isMulti && message.success(i18n("deleteSuccess", { name: node.title }));

            /**
             * A folder has been deleted.
             *
             * @event module:util/hooks#tree/select
             * @param {module:store/TreeNode~TreeNode} node The node
             * @param {object} attachmentsBrowser
             * @this module:AppTree~AppTree
             * @since 4.0.7
             */
            hooks.call("folder/deleted", [node, this.attachmentsBrowser], this);

            // Select parent
            if (!isMulti) {
                const parentId = getTreeParentById(node.id, this.props.store.tree);
                this.handleSelect(parentId === 0 ? +rmlOpts.others.rootId : parentId);
            }
            return true;
        } catch (e) {
            message.error(e.responseJSON.message);
            return false;
        } finally {
            hide();
        }
    };

    /**
     * Handle trashing of multiple categories.
     *
     * @method
     */
    handleMultipleTrash = async () => {
        let count = 0;
        this.setState({ isTreeBusy: true });
        const checkedNodes = getRecursivelyCheckedNodes(this.props.store.tree, true);

        for (const nodes of Object.values(checkedNodes).reverse()) {
            for (const node of nodes) {
                count++;
                const result = await this.handleTrash(null, node, true);

                // An error occured, break the loop...
                if (!result) {
                    this.setState({ isTreeBusy: false });
                    return;
                }
            }
        }

        message.success(i18n("deleteMultipleSuccess", { count }));
        this.setState({ isTreeBusy: false });
        this.handleSelect(+rmlOpts.others.rootId);
    };

    /**
     * Handle manual sorting. The new location is already calculated by the RearrangeBox.
     *
     * @returns boolean
     * @method
     */
    handleSortManual = async (...args) => {
        if (
            process.env.PLUGIN_CTX === "lite" &&
            args.parentToId !== this.props.store.rootId &&
            args.parentToId !== undefined
        ) {
            this.setState({ showProFeature: "subfolder" });
            return false;
        }

        const result = await this.handleSort(...args);
        result &&
            this.setState({
                rearrangeBoxId: false,
                isSortableBusy: false,
                isToolbarBusy: false,
            });
        return result;
    };

    /**
     * Handle categories sorting and update the tree so the changes are visible. If sorting
     * is cancelled the old tree gets restored.
     *
     * @method
     */
    handleSort = async ({ doFinally = true, ...props }) => {
        const { store } = this.props;

        if (process.env.PLUGIN_CTX === "lite" && props.parentToId !== store.rootId) {
            // Revert changes in UI (first move, afterwards cancel to update the observable correctly)
            store.handleSort({
                ...props,
                request: false,
            });
            store.handleSort({
                id: props.id,
                oldIndex: props.newIndex,
                newIndex: props.oldIndex,
                parentFromId: props.parentToId,
                parentToId: props.parentFromId,
                request: false,
            });

            this.setState({ showProFeature: "subfolder" });
            return false;
        }

        this.setState({
            isSortableBusy: true,
            isToolbarBusy: true,
        });

        const hide = message.loading(i18n("sortLoadingText"));
        const { toolbarActiveButton } = this.state;
        const { parentFromId, parentToId } = props;

        try {
            await store.handleSort(props);
            message.success(i18n("sortedSuccess"));

            if (parentFromId === parentToId) {
                /**
                 * This action is called when a folder was relocated in the
                 * folder tree. That means the parent was not changed, only
                 * the order was changed.
                 *
                 * @event module:util/hooks#folder/relocated
                 * @param {object} props The move properties
                 * @this module:AppTree~AppTree
                 * @since 4.0.7
                 */
                hooks.call("folder/relocated", [props], this);
            } else {
                /**
                 * This action is called when a folder was moved in the folder tree.
                 * That means the parent and order was changed.
                 *
                 * @event module:util/hooks#folder/moved
                 * @param {object} props The move properties
                 * @this module:AppTree~AppTree
                 * @since 4.0.7
                 */
                hooks.call("folder/moved", [props], this);
            }
            return true;
        } catch (e) {
            message.error(e.responseJSON.message);
            return false;
        } finally {
            hide();
            doFinally && this._handleSortNode(toolbarActiveButton, false);
        }
    };

    /**
     * Handle responsiveness on window resize.
     *
     * @method
     */
    handleWindowResize = () => {
        const isMobile = this._isMobile();
        this.setState({
            isSticky: !isMobile,
            isStickyHeader: !isMobile,
            isResizable: !isMobile,
            isFullWidth: isMobile,
            style: isMobile ? { marginLeft: 10 } : {},
        });
    };

    /**
     * Handle refesh of content.
     */
    handleReload = () => {
        this.handleSelect();
    };

    handleDestroy() {
        // This needs to be made when rewriting Real Media Library
        // this.ref && unmountComponentAtNode(this.ref.container.parentNode);
    }

    /**
     * Dismiss the import tax notice for a given time (transient).
     *
     * @method
     */
    handleDismissImportTaxNotice = async () => {
        await request({
            location: {
                path: "/notice/import",
                method: "DELETE",
            },
        });
        window.location.reload();
    };

    /**
     * A node item should be an observer (mobx).
     */
    onTreeNodeRender = (createTreeNode, TreeNode, node) => {
        return <Observer key={node.id}>{() => createTreeNode(node, { indeterminate: node.indeterminate })}</Observer>;
    };

    /**
     * A node item icon is present as string.
     */
    onTreeNodeRenderIcon = (icon) => resolveIcon(icon);

    /**
     * Handle rename node states (helper).
     *
     * @method
     */
    _handleRenameNode = (toolbarActiveButton, isCreatableLinkDisabled, isTreeLinkDisabled, nodeRename) => {
        this.setState({
            // Make other nodes editable / not editable
            isCreatableLinkDisabled,
            isTreeLinkDisabled,
            toolbarActiveButton,
        });
        this.updateTreeItemById((node) => {
            // Make selected node editable / not editable
            node.$rename = nodeRename;
        });
    };

    /**
     * Checks if the current window size is mobile.
     *
     * @returns {boolean}
     * @method
     */
    _isMobile = () => $(window).width() <= 700;

    /**
     * Handle the sort node button.
     *
     * @method
     */
    _handleSortNode = (toolbarActiveButton, isBusy) => {
        this.setState({
            isCreatableLinkDisabled: !!toolbarActiveButton,
            toolbarActiveButton,
            isSortableDisabled: !toolbarActiveButton,
            toolbarBackButton: Object.assign(this.state.toolbarBackButton, {
                label: `i18n.${toolbarActiveButton ? "back" : "cancel"}`,
            }),
        });

        typeof isBusy === "boolean" && this.setState({ isSortableBusy: isBusy });
        typeof isBusy === "boolean" && this.setState({ isToolbarBusy: isBusy });
    };

    /**
     * Handle the details meta box.
     */
    _handleDetails = (action, status, response) => {
        const metaBoxId = action ? (action === "usersettings" ? action : this.props.store.selectedId) : false;
        this.setState({ metaBoxId });

        // When the metadata is saved successfully listen to the "reload" state and reload the current view.
        if (status === true && response) {
            const { reload, hardReloadIfBodyHasClass } = response;

            if (reload) {
                this.handleReload();
            } else if (hardReloadIfBodyHasClass && $("body").hasClass(hardReloadIfBodyHasClass)) {
                window.location.reload();
            }
        }
    };

    /**
     * Set the attachments browser location.
     *
     * @param {int} [id=Current selected id] The id
     */
    _handleBackboneFilterSelection(id = this.getSelectedId()) {
        const { attachmentsBrowser } = this;
        if (attachmentsBrowser && id !== ID_NONE) {
            setTimeout(() => {
                const backboneFilter = this.getBackboneFilter();
                backboneFilter && backboneFilter.$el.val(id).change();

                // Reset bulk select in no-modal mode
                attachmentsBrowser.$el.parents(".media-modal").length === 0 &&
                    attachmentsBrowser.controller.state().get("selection").reset();

                // Check if folder needs refresh
                const { store } = this.props;
                if (store.foldersNeedsRefresh.indexOf(id) > -1) {
                    store.removeFoldersNeedsRefresh(id);
                    this.handleReload();
                }
            }, 0);
        }
    }

    /**
     * Update the creatable buttons regarding the selected type.
     *
     * @param {int} selectedCreatableType
     */
    _updateCreatableButtons(selectedCreatableType) {
        this.setState({ selectedCreatableType });

        this.state.availableCreatables.forEach((c) =>
            this.setState({
                [`creatable_${c}`]: Object.assign(this.state[`creatable_${c}`], {
                    visible: this.state[`creatable_${c}`].visibleInFolderType.indexOf(selectedCreatableType) > -1,
                }),
            }),
        );
    }

    _updateToolbarButtons() {
        const { isWPAttachmentsSortMode, toolbar_order, toolbar_rename, toolbar_trash, toolbar_details } = this.state;
        const selected = this.getTreeItemById();
        const disableIfStatic = !selected;
        const restrictions = (selected && selected.properties && selected.properties.restrictions) || [];

        const disableOrder =
            disableIfStatic ||
            isWPAttachmentsSortMode ||
            (selected && selected.contentCustomOrder === 2) ||
            (selected && selected.orderAutomatically);
        toolbar_order.disabled !== disableOrder &&
            this.setState({
                toolbar_order: Object.assign(toolbar_order, {
                    disabled: disableOrder,
                }),
            });

        const disableRename = disableIfStatic || restrictions.indexOf("ren") > -1;
        toolbar_rename.disabled !== disableRename &&
            this.setState({
                toolbar_rename: Object.assign(toolbar_rename, {
                    disabled: disableRename,
                }),
            });

        const disableTrash = disableIfStatic || restrictions.indexOf("del") > -1;
        toolbar_trash.disabled !== disableTrash &&
            this.setState({
                toolbar_trash: Object.assign(toolbar_trash, {
                    disabled: disableTrash,
                }),
            });

        toolbar_details.disabled !== disableIfStatic &&
            this.setState({
                toolbar_details: Object.assign(toolbar_details, {
                    disabled: disableIfStatic,
                }),
            });
    }

    /**
     * Fetch folder tree.
     */
    async fetchTree(setSelectedId) {
        this.setState({ isTreeBusy: true });
        try {
            const { slugs } = await this.props.store.fetchTree(setSelectedId);

            // Modify all available attachments browsers filter
            $(FILTER_SELECTOR).each(function () {
                const backboneFilter = $(this).data("backboneView");

                // Clone to remove immutability
                backboneFilter && backboneFilter.createFilters(JSON.parse(JSON.stringify(slugs)));
            });

            this._handleBackboneFilterSelection();
            latestQueriedFolder.node = this.props.store.selected;
        } catch (e) {
            console.log(e);
        }

        // Modify this tree
        this.setState({ isTreeBusy: false });
    }

    /**
     * Update the folder count. If you pass no argument the folder count is
     * requested from server.
     *
     * @param {object} counts Key value map of folder and count
     */
    async fetchCounts(counts) {
        return await this.props.store.fetchCounts(counts);
    }
}

export default AppTree;
