/** @module hooks/uploader */

import $ from "jquery";
import { Provider } from "mobx-react";
import { createRoot } from "react-dom/client";
import rmlOpts from "rmlopts";
import wp from "wp";

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

import { latestQueriedFolder } from "../components/AppTree.js";
import { ProBox } from "../components/ProFooter.js";
import UploadMessage from "../components/UploadMessage.js";
import { BROWSER_SELECTOR } from "../others/mediaViews.js";
import store from "../store/index.js";
import { basename, dataUriToBlob, dirname, findDeep, hooks, i18n, isHiddenFile } from "../util/index.js";

const UniqueUploadMessage = (
    <Provider store={store}>
        <UploadMessage onCancel={cancelUpload} />
    </Provider>
);
const CLASS_NAME = "ant-message-bottom";
let uploaderFetchCountsTimeout;
let currentMessageHide;
let currentUploader;

/**
 * Cancel all the uploads.
 */
function cancelUpload() {
    currentUploader.stop();
    afterUpload(currentUploader);
    currentUploader.start();
}

/**
 * After upload complete do some required actions.
 */
function afterUpload(up) {
    // Check if we need to reload the complete tree
    const hadRelativePathUploads = up.files.filter((f) => f.getSource().relativePath?.split("/").length > 2).length;

    // Update queue and counter
    up.splice();
    up.total.reset();
    clearTimeout(uploaderFetchCountsTimeout);
    uploaderFetchCountsTimeout = setTimeout(() => {
        if (hadRelativePathUploads) {
            $(BROWSER_SELECTOR).each(function () {
                const backboneView = $(this).data("backboneView");
                if (backboneView) {
                    const { $RmlAppTree } = backboneView.controller;
                    $RmlAppTree.fetchTree();
                }
            });
        } else {
            store.fetchCounts();
        }
    }, 500); // Avoid too many requests

    // Hide uploader message
    hideMessage();
}

/**
 * Show the uploading message.
 *
 * @param {object} up The current plupload instance
 */
function showMessage(up) {
    if (!currentMessageHide) {
        // Only show once
        currentMessageHide = message.loading(UniqueUploadMessage, 0);
    }
    currentUploader = up;
}

/**
 * Hide the uploading message.
 */
function hideMessage() {
    currentMessageHide && currentMessageHide();
    currentMessageHide = undefined;
}

/**
 * Toggle the placement of the unique uploader message.
 */
function togglePlacement() {
    $(this).parents(".ant-message").toggleClass(CLASS_NAME);
    setTimeout(() => $(document).one("mouseenter", ".rml-upload-trigger", togglePlacement), 10);
}

/**
 * Get the current selected node id.
 *
 * @returns {object}
 */
function getNodeId() {
    const selectVal = $(".attachments-filter-preUploadUi:visible:first").prev();
    const id = +selectVal.val();
    return id ? selectVal.data("node") : latestQueriedFolder.node;
}

function createRecursiveUploadProBox() {
    const proBoxElementId = "pro-box-recursive-upload";

    // Avoid duplicate boxes
    if (!document.getElementById(proBoxElementId)) {
        const proBoxElement = document.createElement("div");
        proBoxElement.id = proBoxElementId;
        document.body.append(proBoxElement);
        const root = createRoot(proBoxElement);
        root.render(<ProBox feature="recursive-upload" onClose={() => root.unmount()} />);
    }
}

/**
 * Avoid adding hidden files (why not using `FileAdded`? So we can avoid the security risk notice)
 *
 * @see ttps://github.com/WordPress/WordPress/blob/e8c035f25d7ad99e0e95e2a197c80481a9f9afa4/wp-includes/js/plupload/plupload.js#L1927
 */
function fixAddFileToSkipHiddenFiles(uploader) {
    const oldAddFile = uploader.addFile;
    uploader.addFile = function (file, ...rest) {
        const files = (Array.isArray(file) ? file : [file]).filter((f) => {
            const source = f.getSource();
            if (source && isHiddenFile(f.name)) {
                return false;
            }
            return true;
        });

        oldAddFile.apply(this, [files, ...rest]);
    }.bind(uploader);
}

/**
 * When a file is added do general checks.
 */
hooks.register("uploader/add", function (file, node) {
    if (node.id === "all") {
        this.node = store.getTreeItemById(+rmlOpts.others.rootId, false);
    }
});

/**
 * The media-new.php page. Adds the property to the asyn-upload.php file and
 * modifies the output row while uploading a new file.
 *
 * @see wp-includes/js/plupload/handlers.js
 */
hooks.register("general", () => {
    if (!$("body").hasClass("media-new-php")) {
        return;
    }

    // When the file is uploaded, then the original filename is overwritten. Now we
    // must add it again to the row after the filename.
    if (window.prepareMediaItemInit) {
        const copyPrepareMediaItemInit = window.prepareMediaItemInit;
        window.prepareMediaItemInit = function (file) {
            copyPrepareMediaItemInit.apply(this, arguments);
            if (file.rmlFolderHTML) {
                const mediaRowFilename = $(`#media-item-${file.id}`).find(".filename");
                if (mediaRowFilename.length) {
                    mediaRowFilename.after(file.rmlFolderHTML);
                }
            }
        };
    }

    // Add event to the uploader so the parameter for the folder id is sent
    setTimeout(() => {
        if (window.uploader) {
            fixAddFileToSkipHiddenFiles(window.uploader);

            window.uploader.bind("BeforeUpload", function (up, file) {
                const { multipart_params } = up.settings;
                let title;

                // Always recalculate this fields
                delete multipart_params.rmlCreateFolder;
                delete multipart_params.rmlFolder;

                const rmlFolderNode = getNodeId();

                // Set server-side-readable rmlfolder id
                if (rmlFolderNode && !isNaN(+rmlFolderNode.id)) {
                    multipart_params.rmlFolder = rmlFolderNode.id;

                    // Get title as string
                    const div = document.createElement("div");
                    title = rmlFolderNode.title;
                    typeof title === "string" ? (div.innerText = title) : createRoot(div).render(title);
                    title = div.innerText;
                }

                // Allow uploading files in a folder and recreate the structure
                const source = file.getSource();
                const { relativePath } = source;
                if (relativePath?.split("/").length > 2) {
                    if (process.env.PLUGIN_CTX === "pro") {
                        /* onlypro:start */
                        title = basename(dirname(relativePath));
                        multipart_params.rmlCreateFolder = relativePath;
                        /* onlypro:end */
                    } else {
                        createRecursiveUploadProBox();
                    }
                }

                // Add title of the folder to the row
                const mediaRowFilename = $(`#media-item-${file.id}`).find(".filename");
                if (mediaRowFilename.length > 0) {
                    file.rmlFolderHTML = $("<div />").addClass("media-item-rml-folder").text(title).get(0).outerHTML;
                    mediaRowFilename.after(file.rmlFolderHTML);
                }
            });
        }
    }, 500);
});

/**
 * The default backbone uploader.
 */
hooks.register("general", () => {
    if (!findDeep(window, "wp.media") || !findDeep(window, "wp.Uploader")) {
        return;
    }
    $(document).one("mouseenter", ".rml-upload-trigger", togglePlacement);

    // Initialize
    const oldP = wp.Uploader.prototype;
    const oldInit = oldP.init;
    const oldSucess = oldP.success;
    oldP.init = function () {
        oldInit.apply(this, arguments);

        /**
         * The uploader gets initialized.
         *
         * @event module:util/hooks#uploader/init
         * @this wp.Uploader
         */
        hooks.call("uploader/init", [], this);

        fixAddFileToSkipHiddenFiles(this.uploader);

        // Bind the last selected node to the uploaded file
        this.uploader.bind("FileFiltered", function (up, file) {
            file.rmlFolderNode = getNodeId();
        });

        // A new file is added, add it to the store so it can be rendered
        this.uploader.bind("FilesAdded", function (up, files) {
            showMessage(up);
            files.forEach((file) => {
                const source = file.getSource();
                let relativePath = null;
                if (process.env.PLUGIN_CTX === "pro") {
                    /* onlypro:start */
                    if (source.relativePath && source.relativePath.split("/").length > 2) {
                        relativePath = source.relativePath;
                    }
                    /* onlypro:end */
                }

                const {
                    attachment: { cid },
                    name,
                    percent,
                    loaded,
                    size,
                    rmlFolderNode,
                } = file;
                const previewObj = {
                    cid,
                    name,
                    percent,
                    loaded,
                    size,
                    node: rmlFolderNode,
                    relativePath,
                };

                // If the folder node is not found, do not add the file to our custom uploader UI
                if (!rmlFolderNode) {
                    return;
                }

                /**
                 * A new file is added.
                 *
                 * @event module:util/hooks#uploader/add
                 * @param {object} file The file
                 * @param {module:store/TreeNode~TreeNode} folder The folder node
                 * @param {module:store~Store} store The store
                 * @this object
                 */
                hooks.call("uploader/add", [file, rmlFolderNode, store], previewObj);
                const upload = (file.rmlUpload = store.addUploading(previewObj));

                // Generate preview url
                const preloader = new window.mOxie.Image();
                preloader.onload = () => {
                    preloader.downsize(89, 89);
                    let finalUrl;

                    try {
                        finalUrl = preloader.getAsDataURL();
                        finalUrl = dataUriToBlob(finalUrl);
                        finalUrl = window.URL.createObjectURL(finalUrl);
                        finalUrl && upload.setter((u) => (u.previewSrc = finalUrl));
                    } catch (e) {
                        // Silence is golden.
                    }
                };
                preloader.load(source);
            });
        });

        // Set server-side-readable RML folder id
        this.uploader.bind("BeforeUpload", function (up, file) {
            const { multipart_params } = up.settings;
            const { relativePath } = file.getSource();
            const hasRelativePath = relativePath?.split("/").length > 2;
            let { rmlFolderNode } = file;

            // Always recalculate this fields
            delete multipart_params.rmlCreateFolder;
            delete multipart_params.rmlFolder;

            !rmlFolderNode && (rmlFolderNode = getNodeId()); // Lazy node
            if (rmlFolderNode && !isNaN(+rmlFolderNode.id)) {
                multipart_params.rmlFolder = rmlFolderNode.id;
            }

            // Send relative path to the backend if set, so we can recreate folders recursively
            if (hasRelativePath) {
                if (process.env.PLUGIN_CTX === "pro") {
                    /* onlypro:start */
                    multipart_params.rmlCreateFolder = relativePath;
                    /* onlypro:end */
                } else {
                    createRecursiveUploadProBox();
                }
            }
        });

        // The upload progress
        this.uploader.bind("UploadProgress", function ({ total }, { rmlUpload, percent, loaded }) {
            rmlUpload?.setter((u) => {
                u.percent = percent;
                u.loaded = loaded;
            });

            store.setUploadTotal(total);
        });

        // All files are completed
        this.uploader.bind("UploadComplete", afterUpload);
    };

    /**
     * A single file is completed successfully.
     */
    oldP.success = function (file_attachment) {
        oldSucess.apply(this, arguments);

        // Remove file from queue
        store.removeUploading(file_attachment.cid);
        const uploadId = file_attachment.get("rmlFolderId");
        store.addFoldersNeedsRefresh(uploadId);
        store.addFoldersNeedsRefresh("all"); // Always refresh the "All files" view, too

        // Update all available backbone view
        const rmlGalleryOrder = file_attachment.get("rmlGalleryOrder");
        const at = rmlGalleryOrder === -1 ? 0 : rmlGalleryOrder;
        $(BROWSER_SELECTOR).each(function () {
            const backboneView = $(this).data("backboneView");
            if (backboneView) {
                const {
                    toolbar,
                    controller: { toolbar: bottomToolbar },
                } = backboneView;
                const activeNode = toolbar.secondary.get("rml_folder").model.get("rml_folder");
                if (uploadId === activeNode || activeNode === "") {
                    backboneView.collection.add(file_attachment, { at: activeNode === "" ? 0 : at });

                    // Force to rerender toolbar e. g. to activate "Set featured image" button
                    bottomToolbar && bottomToolbar.render(bottomToolbar.mode());
                }
            }
        });
    };
});

const GALLERY_ALLOWED_EXT = ["jpg", "jpeg", "jpe", "gif", "png"];

/**
 * Checks, if the uploading folder is a collection or gallery and restrict the upload,
 * move the file to unorganized folder.
 */
hooks.register("uploader/add", function ({ name }, { properties }, store) {
    // May only contain image files
    if (properties && properties.type) {
        const ext = name.substr(name.lastIndexOf(".") + 1).toLowerCase();
        const isCollection = +properties.type === 1;
        if ($.inArray(ext, GALLERY_ALLOWED_EXT) === -1 || isCollection) {
            this.node = store.getTreeItemById(+rmlOpts.others.rootId, false);
            this.deny = i18n(isCollection ? "uploadingCollection" : "uploadingGallery");
        }
    }
});

/**
 * Check if the current page is list mode and modify the "Add new" button to
 * preselect in the media-new.php page.
 */
hooks.register("tree/init", function (state, props) {
    const rml_preselect = +state.initialSelectedId;
    if (!props.attachmentsBrowser && !isNaN(rml_preselect)) {
        const a = $('a.page-title-action[href*="/media-new.php"]');
        const href = a.attr("href");
        if (href) {
            const url = new URL(href, window.location.origin);
            url.searchParams.set("rml_preselect", rml_preselect);
            a.attr("href", url.toString());
        }
    }
});
