/** @module store */

import $ from "jquery";
import { Provider, inject, observer } from "mobx-react";
import { clone, flow, getParent, getSnapshot, onPatch, resolveIdentifier, resolvePath, types } from "mobx-state-tree";
import React from "react";

import { updateTreeItemById } from "@devowl-wp/react-folder-tree";

import TreeNode from "./TreeNode.js";
import Upload from "./Upload.js";
import AppTree from "../components/AppTree.js";
import {
    applyNodeDefaults,
    humanFileSize,
    i18n,
    rmlOpts,
    secondsFormat,
    fetchTree as utilFetchTree,
    request as utilsRequest,
} from "../util/index.js";

/**
 * The main Mobx State Tree store for the RML application. It holds a static tree and
 * the fetched tree from the server.
 *
 * @class BasicStore
 * @property {int} [rootId=rmlOpts.others.rootId] The root folder id
 * @property {module:store/TreeNode~TreeNode[]} staticTree The static tree
 * @property {module:store/TreeNode~TreeNode[]} [tree] The tree
 * @property {string|int} [selectedId=0] The selected id
 */
const BasicStore = types
    .model("RMLBasicStore", {
        rootId: +rmlOpts.others.rootId,
        staticTree: types.array(TreeNode),
        tree: types.optional(types.array(TreeNode), []),
        slugs: types.optional(types.frozen(), {
            names: [],
            slugs: [],
            types: [],
        }),
        selectedId: types.optional(types.union(types.string, types.number), 0), // Do not fill manually, it is filled in afterCreated through onPatch
    })
    .views((self) => ({
        /**
         * Get tree item by id.
         *
         * @param {string|int} id
         * @param {boolean} [exlucdeStatic=true]
         * @returns {module:store/TreeNode~TreeNode} Tree node
         * @memberof module:store~BasicStore
         * @instance
         */
        getTreeItemById(id, excludeStatic = true) {
            if (id === false) {
                return undefined;
            }

            const result = resolveIdentifier(TreeNode, self, id);
            if (excludeStatic && self.staticTree.indexOf(result) > -1) {
                return undefined;
            }
            return result;
        },

        get selected() {
            return self.getTreeItemById(self.selectedId, false);
        },

        get breadcrumb() {
            const { selected } = this;
            if (selected) {
                return selected.path.map((node) => node.title);
            } else {
                return [<i key="0">{i18n("noneSelected")}</i>];
            }
        },
    }))
    .actions((self) => {
        let currentlySettingTree = 0; // see onPatch in afterCreate()

        return {
            /**
             * The model is created so watch for specific properties. For example set
             * the selected property.
             *
             * @memberof module:store~BasicStore
             * @private
             * @instance
             */
            afterCreate() {
                onPatch(self, ({ op, path, value }) => {
                    // A new selected item is setted
                    if ((path.startsWith("/tree/") || path.startsWith("/staticTree/")) && path.endsWith("/selected")) {
                        const currentSelected = self.selected;
                        const obj = resolvePath(self, path.slice(0, path.length - 9));
                        if (value === true) {
                            currentSelected &&
                                currentSelected.id !== obj.id &&
                                currentSelected.setter((node) => {
                                    node.selected = false;
                                });
                            self._setSelectedIdFromPath(obj);
                        } else if (currentSelected === obj) {
                            // Reset selected id
                            self._setSelectedIdFromPath({ id: undefined });
                        }
                    } else if (currentlySettingTree === 0 && op === "add" && /(tree|childNodes)\/\d+$/.test(path)) {
                        // Listen to children changes when added to the tree so automatical orders are applied
                        const applyTo = getParent(resolvePath(self, path), 2);
                        if (typeof applyTo.applyChildrenOrder === "function" && applyTo.subOrderAutomatically) {
                            applyTo.applyChildrenOrder();
                        }
                    }
                });
            },

            /**
             * Iterate a callback over all nodes within the static and/or normal tree.
             */
            nodes(fn, isStatic = false) {
                const fnRec = (tree = isStatic ? self.staticTree : self.tree) =>
                    tree.forEach((n) => {
                        fn(n);
                        n.childNodes && fnRec(n.childNodes);
                    });
                fnRec();
            },

            _setSelectedIdFromPath(obj) {
                self.selectedId = obj.id;
            },

            /**
             * Update this node attributes.
             *
             * @param {function} callback The callback with one argument (node draft)
             * @memberof module:store~BasicStore
             * @instance
             */
            setter(callback) {
                callback(self);
            },

            /**
             * Get a snapshot of tree without selection.
             *
             * @returns {object[]}
             */
            getTreeSnapshot() {
                const snapshot = $.extend(true, [], getSnapshot(self.tree));
                updateTreeItemById(self.selectedId, snapshot, (n) => (n.selected = false));
                return snapshot;
            },

            /**
             * Set the tree.
             *
             * @param {object} tree The object representing a tree
             * @param {boolean} [isStatic=false]
             * @param {object} [slugs]
             * @memberof module:store~BasicStore
             * @instance
             */
            setTree(tree, isStatic = false, slugs = null) {
                currentlySettingTree++;
                if (isStatic) {
                    self.staticTree.clear();
                    self.staticTree.replace(tree);
                } else {
                    self.tree.clear();
                    self.tree.replace(tree);
                }

                if (slugs) {
                    self.slugs = slugs;
                }
                currentlySettingTree--;
            },

            /**
             * Handle sort mechanism.
             *
             * @returns {boolean}
             * @throws Error
             * @memberof module:store~Store
             * @instance
             */
            handleSort: flow(function* ({ id, oldIndex, newIndex, parentFromId, parentToId, nextId, request = true }) {
                const { tree, rootId } = self;
                let requestBody = {
                    nextId: nextId === 0 ? false : nextId,
                };

                // Find parent trees with children
                let treeItem;
                if (parentFromId === rootId) {
                    treeItem = tree[oldIndex].toJSON();
                    tree.splice(oldIndex, 1);
                } else {
                    self.getTreeItemById(parentFromId).setter((node) => {
                        treeItem = node.childNodes[oldIndex].toJSON();
                        node.childNodes.splice(oldIndex, 1);
                    }, true);
                }

                if (process.env.PLUGIN_CTX === "pro") {
                    /* onlypro:start */
                    requestBody.parent = parentToId;
                    /* onlypro:end */
                }

                // Find destination tree
                if (parentToId === rootId) {
                    tree.splice(newIndex, 0, treeItem);
                } else {
                    self.getTreeItemById(parentToId).setter((node) => {
                        node.childNodes.splice(newIndex, 0, treeItem);
                    }, true);
                }

                if (!request) {
                    return true;
                }

                // Request
                try {
                    yield utilsRequest({
                        location: {
                            path: `/hierarchy/${id}`,
                            method: "PUT",
                        },
                        request: requestBody,
                    });
                    return true;
                } catch (e) {
                    yield store.handleSort({
                        id,
                        oldIndex: newIndex,
                        newIndex: oldIndex,
                        parentFromId: parentToId,
                        parentToId: parentFromId,
                        request: false,
                    });
                    throw e;
                }
            }),

            /**
             * Fetch the folder tree.
             *
             * @returns {object[]} Tree
             * @memberof module:store~Store
             * @instance
             * @async
             */
            fetchTree: flow(function* (setSelectedId) {
                const { tree, cntRoot, cntAll, slugs } = yield utilFetchTree();
                const result = { tree, cntRoot, cntAll, slugs };
                self.setTree(tree, false, slugs);

                if (typeof setSelectedId !== "undefined") {
                    const node = self.getTreeItemById(setSelectedId, false);
                    node && node.setter((node) => (node.selected = true));
                }

                const all = self.getTreeItemById("all", false);
                all && all.setter((node) => (node.count = cntAll));
                self.getTreeItemById(self.rootId, false).setter((node) => (node.count = cntRoot));
                return result;
            }),

            /**
             * 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
             * @returns {object<string|int,int>} Count map
             * @memberof module:store~Store
             * @instance
             * @async
             */
            fetchCounts: flow(function* (counts) {
                if (counts) {
                    Object.keys(counts).forEach((k) => {
                        const ref = self.getTreeItemById(k, false);
                        ref && (ref.count = counts[k]);
                    });
                    return counts;
                }
                return yield self.fetchCounts(
                    yield utilsRequest({
                        location: {
                            path: "/folders/content/counts",
                        },
                    }),
                );
            }),

            /**
             * Create a new tree node.
             *
             * @param {string} name The name of the new folder
             * @param {object} obj The object representing the folder
             * @param {string|int} obj.parent
             * @param {int} obj.typeInt
             * @param {function} [beforeAttach] Callback executed before attaching the new object to the tree
             * @returns {object} The tree node (no mobx model)
             * @memberof module:store~Store
             * @instance
             * @async
             */
            persist: flow(function* (name, { parent, typeInt }, beforeAttach) {
                const newObj = applyNodeDefaults([
                    yield utilsRequest({
                        location: {
                            path: "/folders",
                            method: "POST",
                        },
                        request: {
                            name,
                            parent,
                            type: typeInt,
                        },
                    }),
                ])[0];

                // Add to tree
                beforeAttach && beforeAttach(newObj);
                if (parent === self.rootId) {
                    self.tree.push(newObj);
                } else if (process.env.PLUGIN_CTX === "pro") {
                    /* onlypro:start */
                    self.getTreeItemById(parent).setter((node) => {
                        node.childNodes.push(newObj);
                    }, true);
                    /* onlypro:end */
                }
                return newObj;
            }),
        };
    });

/**
 * The main Mobx State Tree store for the RML application in the media library view.
 *
 * @class Store
 * @property {mixed[]} [foldersNeedsRefresh] Node ids which needs to be refreshed when they gets queried
 * @property {module:store/Upload~Upload[]} [uploading] The upload queue
 * @property {int} [uploadTotalLoaded=0] The upload total loaded
 * @property {int} [uploadTotalSize=0] The upload total size
 * @property {object} [sortables] Available sortables for the content order menu
 * @property {object} [treeSortables] Available sortables for the tree order menu
 * @property {int} [uploadTotalBytesPerSec=0] The uploader bytes per second
 * @property {module:store/TreeNode~TreeNode} [selected] The selected tree node
 * @property {module:store/Upload~Upload} [currentUpload] The current upload file
 * @property {string} [uploadTotalRemainTime] The current upload remaining time in human readable form
 * @property {string} [readableUploadTotalLoaded] The uploader total loaded in human readable form
 * @property {string} [readableUploadTotalSize] The uploader total size in human readable form
 * @property {string} [readableUploadTotalBytesPerSec] The uploader bytes per second in human readable form
 */

const Store = BasicStore.named("RMLStore")
    .props({
        foldersNeedsRefresh: types.optional(types.array(types.union(types.string, types.number)), []),
        uploading: types.optional(types.array(Upload), []),
        uploadTotalLoaded: types.optional(types.number, 0),
        uploadTotalSize: types.optional(types.number, 0),
        sortables: types.optional(types.frozen()),
        treeSortables: types.optional(types.frozen()),
        uploadTotalBytesPerSec: types.optional(types.number, 0),
    })
    .views((self) => ({
        get currentUpload() {
            return self.uploading.length ? self.uploading[0] : undefined;
        },

        get uploadTotalRemainTime() {
            if (self.uploadTotalBytesPerSec > 0) {
                const remainTime = Math.floor(
                    (self.uploadTotalSize - self.uploadTotalLoaded) / self.uploadTotalBytesPerSec,
                );
                return secondsFormat(remainTime);
            } else {
                return "00:00:00";
            }
        },

        get readableUploadTotalLoaded() {
            return humanFileSize(self.uploadTotalLoaded);
        },

        get readableUploadTotalSize() {
            return humanFileSize(self.uploadTotalSize);
        },

        get readableUploadTotalBytesPerSec() {
            return humanFileSize(self.uploadTotalBytesPerSec);
        },
    }))
    .actions((self) => ({
        /**
         * Set upload total stats.
         *
         * @memberof module:store~Store
         * @instance
         */
        setUploadTotal({ loaded, size, bytesPerSec }) {
            self.uploadTotalLoaded = loaded;
            self.uploadTotalSize = size;
            self.uploadTotalBytesPerSec = bytesPerSec;
        },

        /**
         * Add an uploading file.
         *
         * @param {object} object The object to push
         * @returns {object} The upload instance
         * @memberof module:store~Store
         * @instance
         */
        addUploading(upload) {
            // The tree item needs to be available in the current tree, the upload holds the node so we can safely add it to the tree
            // Imagine: Upload a file directly with "Add media" without the tree ever loaded for the attachment browser
            if (!self.getTreeItemById(upload.node.id, false)) {
                self.tree.push(clone(upload.node));
                self.addFoldersNeedsRefresh(upload.node.id);
            }
            self.uploading.push(upload);
            return self.uploading[self.uploading.length - 1];
        },

        /**
         * Register a folder that it needs refresh.
         *
         * @memberof module:store~Store
         * @instance
         */
        addFoldersNeedsRefresh(id) {
            self.foldersNeedsRefresh.indexOf(id) === -1 && self.foldersNeedsRefresh.push(id);
        },

        /**
         * Register a folder that it needs refresh.
         *
         * @memberof module:store~Store
         * @instance
         */
        removeFoldersNeedsRefresh(id) {
            const idx = self.foldersNeedsRefresh.indexOf(id);
            idx > -1 && self.foldersNeedsRefresh.splice(idx, 1);
        },

        /**
         * Remove an uploading file from queue.
         *
         * @param {string} cid The cid
         * @returns {object} A copy of the original object
         * @memberof module:store~Store
         * @instance
         */
        removeUploading(cid) {
            for (let i = 0; i < self.uploading.length; i++) {
                if (self.uploading[i].cid === cid) {
                    const copy = self.uploading[i].toJSON();
                    self.uploading.splice(i, 1);
                    return copy;
                }
            }
        },
    }));

export const createUnorganizedNode = () => ({
    id: +rmlOpts.others.rootId,
    title: rmlOpts.others.lang.unorganized,
    icon: "home",
    count: 0,
    contentCustomOrder: 2,
    properties: {
        type: 4,
    },
});

export const createAllNode = () => ({
    id: "all",
    title: rmlOpts.others.lang.allPosts,
    icon: "copy",
    count: rmlOpts.others.allPostCnt,
});

/**
 * Main store instance.
 */
const store = Store.create({
    staticTree: [
        {
            id: -2,
            title: "none",
            $visible: false,
            properties: {
                type: -2,
            },
        },
        createAllNode(),
        createUnorganizedNode(),
    ],
    sortables: rmlOpts.others.sortables.content,
    treeSortables: rmlOpts.others.sortables.tree,
});

/**
 * A single instance of store.
 */
export default store;

/**
 * An AppTree implementation with store provided. This means you have no longer
 * implement the Provider of mobx here.
 *
 * @returns {React.Element}
 */
export const StoredAppTree = ({ children, useStore, ...rest }) => (
    <Provider store={useStore ? useStore : store}>
        <AppTree {...rest}>{children}</AppTree>
    </Provider>
);

/**
 * Import general store to ReactJS component.
 */
export function injectAndObserve(fn, store = "store") {
    return inject(store)(observer(fn));
}

export { TreeNode, Upload, BasicStore };
