const ASSET_URL_MEDIA_SELECTOR =
    'img[src^="asset://"], audio[src^="asset://"], video[src^="asset://"], iframe[src^="asset://"]';
const PLACEHOLDER_IMAGE_DATA_URL =
    'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

var $exeTinyMCE = {
    // imagetools is disabled because it generates base64 images
    // colorpicker contextmenu textcolor . Añadidos al core, no hace falta añadir en plugins?
    plugins:
        'tooltips exeaudio edicuatex abcmusic exemindmap exemermaid rssfeed modalwindow exealign exeimage exemedia toggletoolbars exeeffects easyattributes advlist lists autolink exelink charmap preview anchor searchreplace visualchars visualblocks code codemagic fullscreen insertdatetime table paste template hr clearfloat addcontent definitionlist blockquoteandcite pastecode abbr exegames_hangman directionality',
    // These buttons will be visible when the others are hidden
    buttons0:
        'toggletoolbars | undo redo | bold italic | formatselect | alignleft aligncenter alignright alignjustify | exelink unlink | bullist numlist | exeimage exemedia | fullscreen',
    // When buttons0 are hidden, 1, 2 and 3 are visible
    buttons1:
        'toggletoolbars | bold italic | formatselect fontsizeselect fontselect | forecolor backcolor',
    buttons2:
        'alignleft aligncenter alignright alignjustify | template clearfloat addcontent | bullist numlist definitionlist | exelink unlink | outdent indent | blockquote blockquoteandcite | ltr rtl',
    buttons3:
        'undo redo | cut copy paste pastetext | pastehtml pastecode edicuatex | tooltips modalwindow exeeffects | exeimage exemedia | exemindmap exemermaid | exeaudio abcmusic | codemagic | fullscreen',
    browser_spellcheck: true,
    fontsize_formats: '80%=0.8rem 90%=0.9rem 100%=1rem 110%=1.1rem 115%=1.15rem 120%=1.2rem 125%=1.25rem 130%=1.3rem 140%=1.4rem 150%=1.5rem 160%=1.6rem 170%=1.7rem 200%=2rem 250%=2.5rem 300%=3rem',

    menubar: 'edit insert format table tools',
    menu: {
        edit: {
            title: 'Edit',
            items: 'undo redo | selectall searchreplace | cut copy paste pastetext | easyattributes',
        },
        insert: {
            title: 'Insert',
            items: 'template | hr charmap anchor clearfloat addcontent exemermaid | abbr insertdatetime',
        }, // ' | exegames_hangman' removed
        format: {
            title: 'Format',
            items: 'underline strikethrough superscript subscript | formats | removeformat',
        },
        table: {
            title: 'Table',
            items: 'inserttable tableprops deletetable | cell row column',
        },
        tools: {
            title: 'Tools',
            items: 'code codemagic visualchars visualblocks fullscreen',
        },
    },
    contextmenu: 'exelink | inserttable | cell row column deletetable',
    language: 'all', // We set all so we can use eXe's i18n mechanism in all.js,
    edicuatex_url: '/app/common/edicuatex/index.html',
    edicuatex_mathjax_url: '/app/common/exe_math/tex-mml-svg.js',
    getTemplates: function () {
        return [
            {
                title: _('2 columns') + ' 50% 50%',
                description: '',
                url: `libs/tinymce_5/js/tinymce/templates/2-50-50.html`,
            },
            {
                title: _('2 columns') + ' 30% 70%',
                description: '',
                url: `libs/tinymce_5/js/tinymce/templates/2-30-70.html`,
            },
            {
                title: _('2 columns') + ' 70% 30%',
                description: '',
                url: `libs/tinymce_5/js/tinymce/templates/2-70-30.html`,
            },
            {
                title: _('3 columns'),
                description: '',
                url: `libs/tinymce_5/js/tinymce/templates/3.html`,
            },
            {
                title: _('Image (left) + Text (right)'),
                description: '',
                url: `libs/tinymce_5/js/tinymce/templates/img-txt.html`,
            },
            {
                title: _('Text (left) + Image (right)'),
                description: '',
                url: `libs/tinymce_5/js/tinymce/templates/txt-img.html`,
            },
            {
                title: _('Table with column headers'),
                description: '',
                url: `libs/tinymce_5/js/tinymce/templates/table-thead-th.html`,
            },
            {
                title: _('Table with row headers'),
                description: '',
                url: `libs/tinymce_5/js/tinymce/templates/table-tbody-th.html`,
            },
            {
                title: _('Table with column and row headers'),
                description: '',
                url: `libs/tinymce_5/js/tinymce/templates/table-th.html`,
            },
            {
                title: _('2 videos'),
                description: '',
                url: `libs/tinymce_5/js/tinymce/templates/2-videos.html`,
            },
            {
                title: _('2 images'),
                description: '',
                url: `libs/tinymce_5/js/tinymce/templates/2-images.html`,
            },
            {
                title: _('3 images'),
                description: '',
                url: `libs/tinymce_5/js/tinymce/templates/3-images.html`,
            },
        ];
    },
    table_default_styles: {
        width: '100%',
    },

    getAssetURL: function (url) {
        // URL pattern: {basePath}/{version}/path (e.g., /web/exelearning/v0.0.0-alpha/libs/...)
        let assetUrl =
            eXeLearning.config.baseURL +
            eXeLearning.config.basePath +
            '/' +
            eXeLearning.version;
        return assetUrl + url;
    },

    /**
     * Detect the base path for static mode deployments.
     * When deployed to a subdirectory (e.g., /dist/static/), we need to prepend
     * this path to absolute URLs like /app/... and /libs/...
     *
     * Detection works by finding known path markers (/app/, /libs/) in the current pathname.
     *
     * @returns {string} The base path (e.g., '/dist/static') or empty string for root deployment
     */
    getStaticBasePath: function() {
        var pathname = window.location.pathname;
        // Look for /app/ to find where the static deployment ends
        var appIndex = pathname.indexOf('/app/');
        if (appIndex > 0) {
            return pathname.substring(0, appIndex);
        }
        // Also check for /libs/ as fallback (some pages might be in libs/)
        var libsIndex = pathname.indexOf('/libs/');
        if (libsIndex > 0) {
            return pathname.substring(0, libsIndex);
        }
        return '';
    },

    /**
     * Removes Bun-injected scripts from HTML content.
     * Bun injects scripts when serving HTML files for HMR (dev) and bundling (prod).
     *
     * Patterns to filter:
     * - Dev: <script src="/_bun/client/..."> or <script data-bun-dev-server-script>
     * - Prod: <script src="/../../...chunk-*.js"> (relative path traversal to chunk files)
     * - Both: Inline scripts with Bun's visibilitychange/sendBeacon patterns
     *
     * @param {string} html - The HTML content to clean
     * @returns {string} - The cleaned HTML content
     */
    stripBunInjectedScripts: function (html) {
        if (!html) return html;

        // Create a temporary DOM element to parse the HTML
        const temp = document.createElement('div');
        temp.innerHTML = html;

        // Find and remove Bun-injected scripts
        const scripts = temp.querySelectorAll('script');
        scripts.forEach(function (script) {
            const src = script.getAttribute('src') || '';
            const hasBunAttr = script.hasAttribute('data-bun-dev-server-script');

            // Pattern 1: Bun dev server scripts (/_bun/ path or data-bun-dev-server-script attr)
            const isBunDevScript = src.includes('/_bun/') || hasBunAttr;

            // Pattern 2: Bun bundler chunks (relative path traversal to chunk-*.js)
            const isBunChunkScript = /^\/(\.\.\/)+(chunk-[a-z0-9]+\.js)$/i.test(src);

            // Pattern 3: Bun's inline visibilitychange/unref script (no src, contains sendBeacon + /_bun/unref)
            const isBunInlineScript = !src && script.textContent && script.textContent.includes('/_bun/unref');

            if (isBunDevScript || isBunChunkScript || isBunInlineScript) {
                script.remove();
            }
        });

        return temp.innerHTML;
    },

    // Get classes from base.css and style.css
    getAvailableClasses: function () {
        var sheets = document.styleSheets;
        var sheet, rule, rules, item, name, tmp;
        var classes = [];
        var names = [];
        for (var i = 0, iLen = sheets.length; i < iLen; i++) {
            sheet = sheets[i];
            if (
                sheet.href &&
                (sheet.href.indexOf('./base.css') != -1 ||
                    sheet.href.indexOf('./style.css') != -1)
            ) {
                rules = sheet.rules || sheet.cssRules;
                for (var j = 0, jLen = rules.length; j < jLen; j++) {
                    rule = rules[j];
                    tmp = rule.cssText.match(/\.\w+/g);
                    if (tmp) {
                        classes.push.apply(classes, tmp);
                    }
                }
            }
        }
        classes.sort();
        // Add some classes (exe-hidden, etc.)
        var rightClasses = [
            { text: '-- Not Set --', value: '' },
            { text: 'exe-hidden', value: 'exe-hidden' },
            { text: 'exe-hidden-accessible', value: 'exe-hidden-accessible' },
            { text: 'exe-table', value: 'exe-table' },
            { text: 'exe-table-minimalist', value: 'exe-table-minimalist' },
        ];
        for (var z = 0; z < classes.length; z++) {
            name = classes[z].replace('.', '');
            if (
                isNaN(classes[z].charAt(1)) &&
                names.indexOf(name) == -1 &&
                name.indexOf('iDevice') == -1 &&
                name.indexOf('Idevice') == -1 &&
                name != 'js'
            ) {
                rightClasses.push({ text: name, value: name });
                names.push(name);
            }
        }

        return rightClasses;
    },

    rel_list: [
        { title: '---', value: '' },
        { title: 'alternate', value: 'alternate' },
        { title: 'author', value: 'author' },
        { title: 'bookmark', value: 'bookmark' },
        { title: 'external', value: 'external' },
        { title: 'help', value: 'help' },
        { title: 'license', value: 'license' },
        { title: 'lightbox', value: 'lightbox' },
        { title: 'next', value: 'next' },
        { title: 'nofollow', value: 'nofollow' },
        { title: 'prev', value: 'prev' },
    ],
    image_title: true,
    init: function (mode, criteria, hide) {
        this.mode = mode;

        var h = 175;
        if (mode == 'multiple') h = 50;
        var w = 882;
        if (
            typeof $exeTinyMCEToggler.documentWidth == 'undefined' ||
            (typeof $exeTinyMCEToggler.documentWidth != 'undefined' &&
                $exeTinyMCEToggler.documentWidth < 900)
        )
            w = '';

        // FR 303
        var divExists = false;
        if (mode == 'multiple') {
            var div = $(criteria).parent();
            if (
                div.length == 1 &&
                div.attr('class') == 'block' &&
                !div[0].hasAttribute('style')
            ) {
                divExists = true;
                div.css({
                    width: w + 'px',
                    height: h + 50 + 'px',
                    border: '1px solid #C0C0C0',
                }).addClass('hidden-editor');
            }
        }

        tinymce.init({
            language: this.language,
            selector: criteria,
            height: 350,
            convert_urls: false,
            toolbar_mode: 'wrap', // To review. The default toolbar mode, floanting, sometimes causes unexpected errors when editing activities (theme.min.js. line 28818)
            paste_as_text: true,
            schema: this.getSchema(),
            resize: 'both',
            draggable_modal: true,
            branding: false,
            entity_encoding: 'raw',
            remove_trailing_brs: false,
            valid_children: this.getValidChildren(),
            valid_elements: this.getValidElements(),
            extended_valid_elements: this.getExtendedValidElements(),
            fix_list_elements: true,
            plugins: this.plugins,
            menubar: this.menubar,
            menu: this.menu,
            content_css: this.getContentCSS(),
            contextmenu: this.contextmenu,
            browser_spellcheck: this.browser_spellcheck,
            // To review. Uncomment the following line to use relative instead of absolute units to improve accessibility:
            // fontsize_formats: this.fontsize_formats,
            templates: this.getTemplates(),
            table_default_styles: this.table_default_styles,
            table_class_list: this.getAvailableClasses(),
            rel_list: this.rel_list,

            // Math plugin
            // In static/offline mode, use RELATIVE paths (./app/...) which work regardless of subdirectory depth
            // The browser resolves ./app/... relative to the current document location
            // e.g., at /dist/static/index.html → ./app/... resolves to /dist/static/app/...
            // e.g., at /index.html → ./app/... resolves to /app/...
            // In server mode, use getAssetURL() to add version for cache busting
            edicuatex_url: (eXeLearning.config?.isStaticMode || eXeLearning.config?.isOfflineInstallation)
                ? './app/common/edicuatex/index.html'
                : this.getAssetURL(this.edicuatex_url),
            edicuatex_mathjax_url: (eXeLearning.config?.isStaticMode || eXeLearning.config?.isOfflineInstallation)
                ? './app/common/exe_math/tex-mml-svg.js'
                : this.getAssetURL(this.edicuatex_mathjax_url),

            // Images
            image_advtab: true,
            image_title: this.image_title,
            file_browser_callback: function (field_name, url, type) {
                // Open Media Library modal
                const filemanager = window.eXeLearning?.app?.modals?.filemanager;
                if (filemanager) {
                    filemanager.show({
                        onSelect: function(result) {
                            // result = { assetUrl, blobUrl, asset }
                            const field = document.getElementById(field_name);
                            if (field) {
                                field.value = result.assetUrl || result.blobUrl || '';
                                // Trigger change event for TinyMCE to pick up
                                field.dispatchEvent(new Event('change'));
                            }
                        }
                    });
                }
            },
            /* enable automatic uploads of images represented by blob or data URIs*/
            automatic_uploads: true,
            // Prevent TinyMCE from rewriting src/data-mce-src after images_upload_handler
            // resolves. Our handler returns the blob URI (for display) and we manage the
            // blob ↔ asset:// mapping ourselves via resolveAssetUrlsInEditor /
            // prepareHtmlForSync; letting TinyMCE clobber data-mce-src with a blob URL
            // corrupts previously-resolved images when a second asset:// image is inserted
            // (issue #1664).
            images_replace_blob_uris: false,
            file_picker_types: 'file image media',
            /* and here's our custom image picker - opens Media Library modal */
            file_picker_callback: function (cb, value, meta) {
                // Open Media Library modal
                const filemanager = window.eXeLearning?.app?.modals?.filemanager;
                if (filemanager) {
                    filemanager.show({
                        onSelect: function(result) {
                            // result = { assetUrl, blobUrl, asset }

                            // For PDFs, use asset:// URL directly (resolved by asset system)
                            // This avoids TinyMCE converting to base64
                            if (result.asset.mime === 'application/pdf') {
                                cb(result.assetUrl, {
                                    title: result.asset.filename || '',
                                    'data-mce-pdf': 'true'
                                });
                                return;
                            }

                            // For HTML files, bypass TinyMCE's media dialog and insert iframe directly
                            // TinyMCE's media dialog creates <video> elements for all sources, not <iframe>
                            const filename = result.asset.filename || '';
                            if (result.asset.mime === 'text/html' || /\.html?$/i.test(filename)) {
                                // Get the active TinyMCE editor
                                const editor = tinymce.activeEditor;
                                if (editor) {
                                    // Insert iframe directly with asset:// URL
                                    // resolveAssetUrlsInEditor will use resolveHtmlWithAssets() for display
                                    editor.insertContent(
                                        '<iframe src="' + result.assetUrl + '" ' +
                                        'data-mce-html="true" ' +
                                        'style="width:100%; height:600px; border:1px solid #ccc;"></iframe>'
                                    );
                                    // Close the TinyMCE media dialog
                                    editor.windowManager.close();
                                }
                                return;
                            }

                            // Keep asset:// in the editor model; rendering resolves it later.
                            cb(result.assetUrl || result.blobUrl || '', {
                                title: result.asset.filename || '',
                                text: result.asset.filename || '',
                                alt: '',
                                'data-asset-id': result.asset.id
                            });
                        }
                    });
                } else {
                    console.warn('[TinyMCE] Media Library not available');
                }
            },

            // Drag and Drop
            paste_data_images: true,
            // Warn when pasted HTML contains blob: URLs that are not known to the current
            // AssetManager session. Known blobs (in reverseBlobCache) will be handled
            // correctly by images_upload_handler; unknown blobs are stale references that
            // will break and the user should use the Media Library or clone instead.
            paste_preprocess: function (plugin, args) {
                if (!args.content || !/blob:https?:\/\//.test(args.content)) return;
                var assetManager = window.eXeLearning?.app?.project?._yjsBridge?.assetManager;
                if (assetManager?.reverseBlobCache) {
                    var blobs = args.content.match(/blob:https?:\/\/[^\s"'>]+/g) || [];
                    if (blobs.length > 0 && blobs.every(function (blob) { return assetManager.reverseBlobCache.has(blob); })) return;
                }
                if ($exeTinyMCE._blobPasteWarningToast?.toastElement?.isConnected) return;
                var toastsManager = window.eXeLearning?.app?.toasts;
                if (toastsManager) {
                    $exeTinyMCE._blobPasteWarningToast = toastsManager.createToast({
                        icon: 'warning',
                        title: _('Temporary files detected'),
                        body: _('Some files in this content use blob: links, which only work on this page. If you paste this elsewhere, images or files may be missing. To reuse content with files, add them from the File Manager or clone the iDevice.'),
                    });
                }
            },
            // Upload tab?
            image_uploadtab: false,
            images_upload_handler: async function (blobInfo, success, failure) {
                // Check if this is a blob URL from the AssetManager (already stored in IndexedDB)
                const blobUri = blobInfo.blobUri();
                const assetManager = window.eXeLearning?.app?.project?._yjsBridge?.assetManager;

                if (assetManager && blobUri && blobUri.startsWith('blob:')) {
                    if (assetManager.reverseBlobCache.has(blobUri)) {
                        const assetId = assetManager.reverseBlobCache.get(blobUri);
                        // Return the blob URL (not asset://) so TinyMCE can render it.
                        // prepareHtmlForSync converts blob → asset:// on save.
                        success(blobUri, { 'data-asset-id': assetId });
                        return;
                    }
                }

                // Store pasted/dropped images in AssetManager (IndexedDB)
                if (assetManager) {
                    // Reject empty blobs (TinyMCE data:→blob: conversion failures).
                    const blob = blobInfo.blob();
                    if (!blob || blob.size === 0) {
                        console.warn('[TinyMCE] Skipping upload of empty blob (likely a stale placeholder)');
                        failure(_('Error storing image'));
                        return;
                    }

                    $exeTinyMCE.lockScreen();
                    try {
                        const file = new File([blob], blobInfo.filename() || 'image.png', { type: blob.type });
                        const assetUrl = await assetManager.insertImage(file);
                        const assetId = assetManager.extractAssetId(assetUrl);
                        // Register blob → asset mapping; prepareHtmlForSync converts on save.
                        assetManager.reverseBlobCache.set(blobUri, assetId);
                        success(blobUri, { 'data-asset-id': assetId });
                    } catch (err) {
                        console.error('[TinyMCE] Failed to store in AssetManager:', err);
                        failure(_('Error storing image'));
                    }
                    $exeTinyMCE.unlockScreen();
                } else {
                    // AssetManager not available - cannot store image
                    console.error('[TinyMCE] AssetManager not available');
                    failure(_('Media library not available'));
                }
            },

            // Media
            media_alt_source: false,
            media_poster: false,

            // Style Formats (see defaultStyleFormats in tinymce.js)
            style_formats: [
                {
                    title: 'Headings',
                    items: [
                        { title: 'Heading 1', format: 'h1' },
                        { title: 'Heading 2', format: 'h2' },
                        { title: 'Heading 3', format: 'h3' },
                        { title: 'Heading 4', format: 'h4' },
                        { title: 'Heading 5', format: 'h5' },
                        { title: 'Heading 6', format: 'h6' },
                    ],
                },
                {
                    title: _('Inline'),
                    items: [
                        { title: _('Bold'), icon: 'bold', format: 'bold' },
                        {
                            title: _('Italic'),
                            icon: 'italic',
                            format: 'italic',
                        },
                        {
                            title: _('Underline'),
                            icon: 'underline',
                            format: 'underline',
                        },
                        {
                            title: _('Strikethrough'),
                            icon: 'strike-through',
                            format: 'strikethrough',
                        },
                        {
                            title: _('Superscript'),
                            icon: 'superscript',
                            format: 'superscript',
                        },
                        {
                            title: _('Subscript'),
                            icon: 'subscript',
                            format: 'subscript',
                        },
                        {
                            title: _('Code'),
                            icon: 'sourcecode',
                            format: 'code',
                        },
                        // Deletion, Insertion and Cite are not part of the default list
                        { title: _('Deletion') + ': <del>', inline: 'del' },
                        { title: _('Insertion') + ': <ins>', inline: 'ins' },
                        { title: _('Cite') + ': <cite>', inline: 'cite' },
                        {
                            title: _('Highlighted text') + ': <mark>',
                            inline: 'mark',
                        },
                    ],
                },
                {
                    title: 'Blocks',
                    items: [
                        { title: 'Paragraph', format: 'p' },
                        { title: 'Blockquote', format: 'blockquote' },
                        { title: 'Div', format: 'div' },
                        { title: 'Pre', format: 'pre' },
                    ],
                },
                {
                    title: 'Alignment',
                    items: [
                        {
                            title: 'Left',
                            icon: 'align-left',
                            format: 'alignleft',
                        },
                        {
                            title: 'Center',
                            icon: 'align-center',
                            format: 'aligncenter',
                        },
                        {
                            title: 'Right',
                            icon: 'align-right',
                            format: 'alignright',
                        },
                        {
                            title: 'Justify',
                            icon: 'align-justify',
                            format: 'alignjustify',
                        },
                    ],
                },
            ],
            style_formats_merge: false, // Overwrite default style formats
            toolbar: [
                this.buttons0,
                this.buttons1,
                this.buttons2,
                this.buttons3,
            ],
            setup: function (ed) {
                ed.on('BeforeSetContent', function(e) {
                    if (typeof e.content !== 'string') return;

                    e.content = $exeTinyMCE.prepareContentForEditorLoad(e.content);
                });

                // Register SetContent handler BEFORE content is loaded
                // This is critical for resolving asset:// URLs in the initial content
                ed.on('SetContent', function(e) {
                    // Strip Bun-injected scripts from template content
                    // This handles both dev (/_bun/client/) and prod (/../../chunk-) patterns
                    // Only process non-initial content to avoid processing on editor load
                    if (!e.initial && !e.paste) {
                        const body = ed.getBody();
                        if (body) {
                            const scripts = body.querySelectorAll('script');
                            scripts.forEach(function (script) {
                                const src = script.getAttribute('src') || '';
                                const hasBunAttr = script.hasAttribute('data-bun-dev-server-script');
                                const isBunDevScript = src.includes('/_bun/') || hasBunAttr;
                                const isBunChunkScript = /^\/(\.\.\/)+(chunk-[a-z0-9]+\.js)$/i.test(src);
                                const isBunInlineScript = !src && script.textContent && script.textContent.includes('/_bun/unref');

                                if (isBunDevScript || isBunChunkScript || isBunInlineScript) {
                                    script.remove();
                                }
                            });
                        }
                    }

                    $exeTinyMCE.resolveAssetUrlsInEditor(ed);
                });

                ed.on('GetContent', function(e) {
                    const assetManager = window.eXeLearning?.app?.project?._yjsBridge?.assetManager;
                    if (assetManager?.prepareHtmlForSync && typeof e.content === 'string') {
                        e.content = assetManager.prepareHtmlForSync(e.content);
                    }
                });
            },
            init_instance_callback: function (ed) {
                if (mode == 'multiple') {
                    if (divExists) div.removeAttr('style'); // FR 303
                    $exeTinyMCEToggler.init(ed.id, hide);
                }

                // Intercept image dialog so users see asset:// URLs instead of blob://.
                // Must run after init when windowManager is available.
                $exeTinyMCE._patchAssetDialogs(ed);

                // Hook for Yjs collaborative editing - bind editor if Yjs is enabled
                if (typeof $exeTinyMCE.onEditorInit === 'function') {
                    $exeTinyMCE.onEditorInit(ed);
                }

                // Note: SetContent handler is now registered in setup() callback
                // to catch the initial content load before init_instance_callback runs

                // Also observe DOM changes for dynamically inserted media (e.g., images, audio recorder, PDF embed)
                const editorBody = ed.getBody();
                if (editorBody) {
                    const observer = new MutationObserver(function(mutations) {
                        let hasNewMedia = false;
                        for (const mutation of mutations) {
                            if (mutation.type === 'childList') {
                                for (const node of mutation.addedNodes) {
                                    if (node.nodeType === 1) {
                                        const hasAssetUrl = node.querySelector?.(ASSET_URL_MEDIA_SELECTOR) ||
                                            (node.matches?.(ASSET_URL_MEDIA_SELECTOR));
                                        if (hasAssetUrl) {
                                            hasNewMedia = true;
                                            break;
                                        }
                                    }
                                }
                            }
                            if (hasNewMedia) break;
                        }
                        if (hasNewMedia) {
                            $exeTinyMCE.resolveAssetUrlsInEditor(ed);
                        }
                    });
                    observer.observe(editorBody, { childList: true, subtree: true });

                    // Clean up observer when editor is removed
                    ed.on('remove', function() {
                        observer.disconnect();
                    });
                }
            },
        }); //End tinymce
    },

    /**
     * Patch TinyMCE's windowManager so the image dialog shows asset:// URLs
     * instead of ephemeral blob:// URLs. On submit, waits for dialog close then
     * re-resolves asset:// → blob:// via resolveAssetUrlsInEditor.
     */
    _patchAssetDialogs: function (ed) {
        if (!ed.windowManager || ed.windowManager._assetPatched) return;
        ed.windowManager._assetPatched = true;
        var origOpen = ed.windowManager.open;
        ed.windowManager.open = function (spec) {
            var assetManager = window.eXeLearning?.app?.project?._yjsBridge?.assetManager;
            if (!assetManager) return origOpen.apply(this, arguments);

            var needsResolveAfterClose = false;

            // --- Image dialog: spec.initialData.src ---
            var src = spec.initialData?.src;
            if (src && typeof src.value === 'string' && src.value.startsWith('blob:')) {
                var assetId = assetManager.reverseBlobCache.get(src.value);
                if (assetId) {
                    var metadata = assetManager.getAssetMetadata?.(assetId);
                    var filename = metadata?.filename || metadata?.name;
                    src.value = assetManager.getAssetUrl?.(assetId, filename) || ('asset://' + assetId);
                    needsResolveAfterClose = true;
                }
            }

            // --- Media dialog (exemedia): spec.initialData.source ---
            var source = spec.initialData?.source;
            if (source && typeof source.value === 'string' && source.value.startsWith('blob:')) {
                var assetId = assetManager.reverseBlobCache.get(source.value);
                if (assetId) {
                    var metadata = assetManager.getAssetMetadata?.(assetId);
                    var filename = metadata?.filename || metadata?.name;
                    source.value = assetManager.getAssetUrl?.(assetId, filename) || ('asset://' + assetId);
                    needsResolveAfterClose = true;
                }
            }

            if (needsResolveAfterClose) {
                var origSubmit = spec.onSubmit;
                spec.onSubmit = function (api) {
                    var node = ed.selection.getNode();
                    if (node?.tagName === 'IMG') {
                        node.removeAttribute('data-asset-src');
                    }
                    if (origSubmit) origSubmit.call(this, api);
                    var handler = function () {
                        ed.off('CloseWindow', handler);
                        setTimeout(function () {
                            var body = ed.getBody();
                            if (!body) return;
                            body.querySelectorAll(ASSET_URL_MEDIA_SELECTOR).forEach(function (el) {
                                el.removeAttribute('data-asset-src');
                            });
                            $exeTinyMCE.resolveAssetUrlsInEditor(ed);
                        }, 50);
                    };
                    ed.on('CloseWindow', handler);
                };
            }

            return origOpen.apply(this, arguments);
        };
        // Clean up flag when editor is destroyed so re-init can re-patch
        ed.on('remove', function () { delete ed.windowManager._assetPatched; });
    },

    prepareContentForEditorLoad: function (content) {
        if (typeof content !== 'string') return content;

        var result = content;

        // Recover stale blob:// URLs in src attributes to asset:// so
        // resolveAssetUrlsInEditor can re-resolve them from the asset store.
        // Blob URLs don't survive page reload, clone, or navigation.
        if (result.includes('blob:')) {
            var assetManager = window.eXeLearning?.app?.project?._yjsBridge?.assetManager;
            if (assetManager) {
                result = result.replace(
                    /(<(?:img|audio|video|source)\b[^>]*?)src=(["'])(blob:[^"']+)\2/gi,
                    function (match, before, quote, blobUrl) {
                        var assetId = assetManager.reverseBlobCache.get(blobUrl);
                        if (assetId) {
                            var assetUrl = assetManager.getAssetUrl?.(assetId) || ('asset://' + assetId);
                            return before + 'src=' + quote + assetUrl + quote;
                        }
                        // Fallback: recover from data-asset-id when blob is not in reverseBlobCache
                        var idMatch = before.match(/data-asset-id=(["'])([^"']+)\1/);
                        if (idMatch) {
                            return before + 'src=' + quote + 'asset://' + idMatch[2] + quote;
                        }
                        return match;
                    }
                );
            }
        }

        if (!result.includes('asset://')) return result;

        // Strip data-mce-src="asset://..." — TinyMCE treats it as canonical src and
        // would override the blob:// resolved by resolveAssetUrlsInEditor.
        // Strip data-asset-src — runtime tracking attr that resolveAssetUrlsInEditor
        // uses as a skip guard; stale persisted values would permanently block resolution.
        return result.replace(/\s+(?:data-mce-src=(["'])asset:\/\/[^"']*\1|data-asset-src=(["'])[^"']*\2)/g, '');
    },

    getSchema: function () {
        var s = 'html5';
        return s;
    },

    getContentCSS: function () {
        // Fallback theme path if theme not yet selected (timing issue during iDevice loading)
        var themePath = eXeLearning.app.themes.selected?.path || '/files/perm/themes/base/INTEF/';
        return (
            eXeLearning.app.api.apiUrlBase +
            '/app/editor/tinymce_5_extra.css,' +
            eXeLearning.app.api.apiUrlBase +
            '/libs/bootstrap/bootstrap.min.css,' +
            themePath +
            'style.css'
        );
    },

    getValidElements: function () {
        var e = '*[*]';
        return e;
    },

    getValidChildren: function () {
        var v = '+body[style]';
        return v;
    },

    getExtendedValidElements: function () {
        var e = '';
        return e;
    },

    // Tracks the active blob-paste warning toast to avoid duplicates
    _blobPasteWarningToast: null,

    lockScreen: function () {
        const loadScreen = document.getElementById('load-screen-node-content');
        loadScreen.style.zIndex = '9999';
        loadScreen.style.position = 'fixed';
        loadScreen.style.top = '0';
        loadScreen.style.left = '0';
        loadScreen.classList.remove('hide', 'hidden');
        loadScreen.classList.add('loading');
    },

    unlockScreen: function (delay = 1000) {
        delay = delay > 1000 ? 400 : 0;
        const loadScreen = document.getElementById('load-screen-node-content');
        loadScreen.classList.remove('loading');
        loadScreen.classList.add('hidding');
        setTimeout(() => {
            loadScreen.classList.add('hide', 'hidden');
            loadScreen.classList.remove('hidding');
            loadScreen.style.zIndex = '990';
            loadScreen.style.position = 'absolute';
            delete loadScreen.style.top;
            delete loadScreen.style.left;
        }, delay);
    },

    /**
     * Resolve asset:// URLs to blob:// URLs for editor-rendered media in TinyMCE.
     * This allows images and media to render within the editor while keeping asset://
     * URLs as the persisted format.
     *
     * NOTE: We intentionally DO NOT resolve iframes (PDFs) because:
     * 1. TinyMCE strips custom attributes like data-asset-src when processing media elements
     * 2. This causes the blob:// URL to be saved instead of asset://
     * 3. PDFs don't need playback preview in the editor - they display correctly in preview mode
     *
     * @param {Object} ed - TinyMCE editor instance
     */
    resolveAssetUrlsInEditor: function (ed) {
        const assetManager = window.eXeLearning?.app?.project?._yjsBridge?.assetManager;
        if (!assetManager) return;

        const body = ed.getBody();
        if (!body) return;

        // Wrap src mutations so they don't create undo levels (visual-only change).
        function setSrcSilently(el, val) {
            if (ed.undoManager?.ignore) {
                ed.undoManager.ignore(function () { el.setAttribute('src', val); });
            } else {
                el.setAttribute('src', val);
            }
        }

        // Find image, audio, video, and iframe elements with asset:// URLs
        const mediaElements = body.querySelectorAll(ASSET_URL_MEDIA_SELECTOR);

        for (const media of mediaElements) {
            const assetUrl = media.getAttribute('src');
            if (!assetUrl || !assetUrl.startsWith('asset://')) continue;

            const isIframe = media.tagName.toLowerCase() === 'iframe';
            const isImage = media.tagName.toLowerCase() === 'img';

            // For images/audio/video: Skip if already resolved (has data-asset-src)
            // For iframes: Skip if src is already a blob URL (already resolved)
            if (!isIframe && media.getAttribute('data-asset-src')) continue;
            if (isIframe && media.getAttribute('src').startsWith('blob:')) continue;

            // For images/audio/video: Store the original asset URL in data-asset-src
            // For iframes: DON'T add data-asset-src - TinyMCE preserves the URL via data-mce-p-src
            // on the parent span.mce-preview-object
            if (!isIframe) {
                media.setAttribute('data-asset-src', assetUrl);
            }

            // Check if this is an HTML iframe (needs full resolution with internal URLs)
            const isHtmlIframe = isIframe && (
                media.getAttribute('data-mce-html') === 'true' ||
                assetUrl.match(/\.html?$/i)
            );

            if (isHtmlIframe) {
                // For HTML iframes, store original asset URL before resolving
                // This is needed because TinyMCE doesn't wrap directly-inserted iframes in span.mce-preview-object
                media.setAttribute('data-asset-src', assetUrl);

                // For HTML iframes, use resolveHtmlWithAssets to resolve internal URLs
                const assetIdMatch = assetUrl.match(/asset:\/\/([a-f0-9-]+)/i);
                if (assetIdMatch) {
                    assetManager.resolveHtmlWithAssets(assetIdMatch[1]).then(function(resolvedUrl) {
                        if (resolvedUrl) {
                            setSrcSilently(media, resolvedUrl);
                            // Register in reverseBlobCache so convertBlobUrlsToAssetUrls can restore it
                            assetManager.reverseBlobCache.set(resolvedUrl, assetIdMatch[1]);
                        }
                    }).catch(function(err) {
                        console.warn('[TinyMCE] Failed to resolve HTML asset:', assetUrl, err);
                    });
                }
            } else {
                if (isImage) {
                    // Keep asset:// as data-mce-src so TinyMCE serialises the original
                    // URL on GetContent if the blob resolves after the user saves.
                    // Do NOT set PLACEHOLDER_IMAGE_DATA_URL here: TinyMCE automatic_uploads
                    // would convert any data: src to blob: and call images_upload_handler,
                    // which would store the placeholder as a fake project asset.
                    media.setAttribute('data-mce-src', assetUrl);
                }

                // Resolve to blob URL asynchronously (for images, audio, video, PDF iframes)
                assetManager.resolveAssetURL(assetUrl).then(function(blobUrl) {
                    if (blobUrl) {
                        setSrcSilently(media, blobUrl);
                        // For images: keep data-mce-src = asset:// (set above) so
                        // TinyMCE dialog shows the canonical URL, not the blob.
                    }
                }).catch(function(err) {
                    console.warn('[TinyMCE] Failed to resolve asset URL:', assetUrl, err);
                });
            }
        }

        // Also handle TinyMCE's mce-preview-object spans (used for media preview)
        const previewSpans = body.querySelectorAll('span.mce-preview-object[data-mce-p-src^="asset://"]');

        for (const span of previewSpans) {
            const assetUrl = span.getAttribute('data-mce-p-src');
            if (!assetUrl || !assetUrl.startsWith('asset://')) continue;

            // Find the inner media element (audio, video, or iframe)
            const innerMedia = span.querySelector('audio, video, iframe');
            if (!innerMedia) continue;

            const isIframe = innerMedia.tagName.toLowerCase() === 'iframe';

            // For audio/video: Skip if already resolved (has data-asset-src)
            // For iframes: Skip if src is already a blob URL
            if (!isIframe && innerMedia.getAttribute('data-asset-src')) continue;
            if (isIframe && innerMedia.getAttribute('src')?.startsWith('blob:')) continue;

            // Store the original asset URL in data-asset-src for all media types
            // This ensures the asset URL is preserved even if TinyMCE doesn't wrap in mce-preview-object
            innerMedia.setAttribute('data-asset-src', assetUrl);

            // Check if this is an HTML iframe (needs full resolution with internal URLs)
            const isHtmlIframe = isIframe && (
                innerMedia.getAttribute('data-mce-html') === 'true' ||
                assetUrl.match(/\.html?$/i)
            );

            if (isHtmlIframe) {
                // For HTML iframes, use resolveHtmlWithAssets to resolve internal URLs
                const assetIdMatch = assetUrl.match(/asset:\/\/([a-f0-9-]+)/i);
                if (assetIdMatch) {
                    assetManager.resolveHtmlWithAssets(assetIdMatch[1]).then(function(resolvedUrl) {
                        if (resolvedUrl) {
                            setSrcSilently(innerMedia, resolvedUrl);
                            // Register in reverseBlobCache so convertBlobUrlsToAssetUrls can restore it
                            assetManager.reverseBlobCache.set(resolvedUrl, assetIdMatch[1]);
                        }
                    }).catch(function(err) {
                        console.warn('[TinyMCE] Failed to resolve HTML asset in preview:', assetUrl, err);
                    });
                }
            } else {
                // Resolve to blob URL asynchronously (for audio, video, PDF iframes)
                assetManager.resolveAssetURL(assetUrl).then(function(blobUrl) {
                    if (blobUrl) {
                        setSrcSilently(innerMedia, blobUrl);
                    }
                }).catch(function(err) {
                    console.warn('[TinyMCE] Failed to resolve asset URL in preview:', assetUrl, err);
                });
            }
        }
    },
};

/* This will run when having more than one TEXTAREA in a page */
var $exeTinyMCEToggler = {
    mode: 'always', // "always" if you don't want to have contents without HTML (plain textareas). Any other value if you do.

    setup: function (eds) {
        eds.each(function () {
            var n = this.name;
            this.id = n;
            var e = $(this);
            if ($exeTinyMCEToggler.mode == 'always') {
                $exeTinyMCEToggler.createViewer(e);
            } else {
                if (e.val() == '' || e.val() == e.html()) {
                    // No HTML (plain textarea)
                    e.css({
                        border: '1px solid #ccc',
                        height: '1.5em',
                        width: '100%',
                        padding: '15px',
                        margin: '0 0 1.5em 0',
                    });
                    // To review $exeTinyMCEToggler.createEditorLink(e,n);
                } else {
                    $exeTinyMCEToggler.createViewer(e);
                }
            }
        });
    },

    createViewer: function (e) {
        if (typeof this.documentWidth == 'undefined') {
            this.documentWidth = $(document).width();
        }
        var id = e.attr('id');
        var n = e.attr('name');
        var c = e.val();
        var w = ';width:852px';
        if (this.documentWidth < 900) w = '';
        // To review: $exeTinyMCEToggler.createEditorLink(e,n);
        var v = $(
            '<div class="exe-textarea-preview" id="' +
                id +
                '-viewer" style="height:96px;padding:2px 15px;border:1px solid #ccc;overflow:auto' +
                w +
                '" onclick="$exeTinyMCEToggler.removeViewer(\'' +
                id +
                '\')">' +
                c +
                '</div>'
        );
        e.before(v).addClass('sr-av'); // If we use e.hide() TinyMCE won't be properly displayed
    },

    removeViewer: function (id) {
        $('#' + id + '-toggler').remove();
        this.startEditor(id, true);
    },

    getHelpLink: function (e) {
        // The textarea has a label with an ID: textareaID-editor-label
        var w = $('#' + e.attr('id') + '-editor-label');
        if (w.length > 0) return w;

        // Get the help link to insert the TinyMCE toggler after it
        var r = '';

        // Multi-choice, etc.
        var p = e.parent().parent().prev();
        var f = $('A', p).eq(0); // f = First link in p, being p the previous element (it can be different in each case)
        if (f.length == 1 && f.html().indexOf('/images/help.gif') != -1) {
            return f;
        }

        // Look for the help link in the previous .block
        p = e.parent().prev('.block');
        f = $('A', p).eq(0);
        if (f.length == 1 && f.html().indexOf('/images/help.gif') != -1) {
            return f;
        }

        // Multi-select question has a different HTML. We look for the help link.
        p = e.parent();
        f = $('A', p).eq(0);
        if (f.length == 1 && f.html().indexOf('/images/help.gif') != -1) {
            return f;
        }

        // Case Study...
        p = e.parent().prev().prev().prev();
        if (p.length == 1) {
            var h = p.attr('href');
            if (h && h.indexOf('Javascript:void') == 0) {
                return p;
            }
        }

        return r;
    },

    createEditorLink: function (e, id) {
        var f = this.getHelpLink(e);
        if (f != '') {
            var l = $(
                '<a href="#" id="' +
                    id +
                    '-toggler" onclick="$exeTinyMCEToggler.startEditor(\'' +
                    id +
                    '\',false);$(this).remove();return false" class="exe-editor-toggler visible-editor">' +
                    _('Editor') +
                    '</a>'
            );
            f.css('margin-right', '55px').after(l);
        } else {
            // We can't find the help link, so be just enable the editor
            // $exeTinyMCEToggler.startEditor(id,false);
        }
    },

    startEditor: function (id, hide) {
        $('#' + id + '-viewer').remove();
        $('#' + id).show();
        $exeTinyMCE.init('multiple', '#' + id, hide);
    },

    addLinkAndToggle: function (id, lab, l, hide) {
        lab.css('margin-right', '5px').after(l);
        if (hide != false)
            this.toggle(id, document.getElementById(id + '-toggler'));
    },

    init: function (id, hide) {
        var e = $('#' + id);
        var f = this.getHelpLink(e);
    },

    toggle: function (id, e) {
        var p = $('#' + id).parent();
        window[e.id + '-iframeHeight'] = '134px';
        var t = $('IFRAME', p);
        var i = '';
        if (t.length == 1) i = t.eq(0);
        if ($(e).hasClass('visible-editor')) {
            // Hide toolbars
            if (i != '') {
                window[e.id + '-iframeHeight'] = i.css('height');
                var w = i.css('width');
                if (parseInt(w.replace('px', '')) < 500) w = '700px';
                i.css('width', w);
                var h = parseInt(window[e.id + '-iframeHeight'].replace('px'));
                h = 100; // Force height: Comment this line to make it as high as the TinyMCE editor
                i.css('height', h + 'px');
            }
            $(e).removeClass('visible-editor').addClass('hidden-editor');
            p.addClass('hidden-editor');
            $('.mce-edit-area').css('border-width', '0'); // So the box doesn't have a 2px border top
        } else {
            // Show toolbars
            if (i != '') i.css('height', window[e.id + '-iframeHeight']);
            $(e).removeClass('hidden-editor').addClass('visible-editor');
            $('.mce-edit-area').css('border-width', '1px 0 0');
            p.removeClass('hidden-editor');
        }
    },
};

// Export for Node.js/CommonJS (tests)
if (typeof module !== 'undefined' && module.exports) {
    module.exports = { $exeTinyMCE, $exeTinyMCEToggler };
}
