// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later

define([
    'jquery',
    '/common/modes.js',
    '/common/themes.js',
    '/customize/messages.js',
    '/common/common-ui-elements.js',
    '/common/inner/common-mediatag.js',
    '/common/common-hash.js',
    '/common/common-util.js',
    '/common/text-cursor.js',
    '/components/chainpad/chainpad.dist.js',
    '/common/hyperscript.js',
], function ($, Modes, Themes, Messages, UIElements, MT, Hash, Util, TextCursor, ChainPad, h) {
    var module = {};

     var cursorToPos = module.cursorToPos = function(cursor, oldText) {
        var cLine = cursor.line;
        var cCh = cursor.ch;
        var pos = 0;
        var textLines = oldText.split("\n");
        for (var line = 0; line <= cLine; line++) {
            if(line < cLine) {
                pos += textLines[line].length+1;
            }
            else if(line === cLine) {
                pos += cCh;
            }
        }
        return pos;
    };

    var posToCursor = module.posToCursor = function(position, newText) {
        var cursor = {
            line: 0,
            ch: 0
        };
        var textLines = newText.substr(0, position).split("\n");
        cursor.line = textLines.length - 1;
        cursor.ch = textLines[cursor.line].length;
        return cursor;
    };

    module.getContentExtension = function (mode) {
        var ext = Modes.extensionOf(mode);
        return ext !== undefined ? ext : '.txt';
    };
    module.fileExporter = function (content) {
        return new Blob([ content ], { type: 'text/plain;charset=utf-8' });
    };
    module.setValueAndCursor = function (editor, oldDoc, remoteDoc) {
        editor._noCursorUpdate = true;
        var scroll = editor.getScrollInfo();
        //get old cursor here
        var oldCursor = {};
        oldCursor.selectionStart = cursorToPos(editor.getCursor('from'), oldDoc);
        oldCursor.selectionEnd = cursorToPos(editor.getCursor('to'), oldDoc);

        editor.setValue(remoteDoc);
        editor.save();

        var ops = ChainPad.Diff.diff(oldDoc, remoteDoc);
        var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
            return TextCursor.transformCursor(oldCursor[attr], ops);
        });

        editor._noCursorUpdate = false;
        editor.scrollTo(scroll.left, scroll.top);

        if (!editor.hasFocus()) { return; }

        if(selects[0] === selects[1]) {
            editor.setCursor(posToCursor(selects[0], remoteDoc));
        }
        else {
            editor.setSelection(posToCursor(selects[0], remoteDoc), posToCursor(selects[1], remoteDoc));
        }

        editor.scrollTo(scroll.left, scroll.top);
    };

    module.handleImagePaste = function (editor) {
        // Don't paste file path in the users wants to paste a file
        editor.on('paste', function (editor, ev) {
            try {
                if (!ev.clipboardData.items) { return; }
                var items = Array.prototype.slice.apply(ev.clipboardData.items);
                var hasFile = items.some(function (el) {
                    return el.kind === "file";
                });
                if (!hasFile) { return; }
                ev.preventDefault();
            } catch (e) { console.error(e); }
        });
    };

    module.getHeadingText = function (editor) {
        var lines = editor.getValue().split(/\n/);

        var text = '';
        lines.some(function (line) {
            // lines including a c-style comment are also valuable
            var clike = /^\s*(\/\*|\/\/)(.*)?(\*\/)*$/;
            if (clike.test(line)) {
                line.replace(clike, function (a, one, two) {
                    if (!(two && two.replace)) { return; }
                    text = two.replace(/\*\/\s*$/, '').trim();
                });
                return true;
            }

            // lisps?
            var lispy = /^\s*(;|#\|)+(.*?)$/;
            if (lispy.test(line)) {
                line.replace(lispy, function (a, one, two) {
                    text = two;
                });
                return true;
            }

            // lines beginning with a hash are potentially valuable
            // works for markdown, python, bash, etc.
            var hash = /^#+(.*?)$/;
            var hashAndLink = /^#+\s*\[(.*?)\]\(.*\)\s*$/;
            if (hash.test(line)) {
                // test for link inside the title, and set text just to the name of the link
                if (hashAndLink.test(line)) {
                    line.replace(hashAndLink, function (a, one) {
                        text = Util.stripTags(one);
                    });
                    return true;
                }
                line.replace(hash, function (a, one) {
                    text = Util.stripTags(one);
                });
                return true;
            }

            // TODO make one more pass for multiline comments
        });

        return text.trim();
    };

    var isMobile = /Android|iPhone/i.test(navigator.userAgent);

    module.mkIndentSettings = function (editor, metadataMgr) {
        var setIndentation = function (units, useTabs, fontSize, spellcheck, brackets) {
            if (typeof(units) !== 'number') { return; }
            var doc = editor.getDoc();
            if (isMobile && fontSize < 16) {
                fontSize = 16;
            }
            editor.setOption('indentUnit', units);
            editor.setOption('tabSize', units);
            editor.setOption('indentWithTabs', useTabs);
            editor.setOption('spellcheck', spellcheck);
            editor.setOption('autoCloseBrackets', brackets);
            setTimeout(function () {
                $('.CodeMirror').css('font-size', fontSize+'px');
                editor.refresh();
            });

            // orgmode is using its own shortcuts
            if (editor.getMode().name === 'orgmode') { return; }
            editor.setOption("extraKeys", {
                Tab: function() {
                    if (doc.somethingSelected()) {
                        editor.execCommand("indentMore");
                    }
                    else {
                        if (!useTabs) { editor.execCommand("insertSoftTab"); }
                        else { editor.execCommand("insertTab"); }
                    }
                },
                //remove focus from editor
                "Esc": function () {
                    editor.display.input.blur();
                },
                "Shift-Tab": function () {
                    editor.execCommand("indentLess");
                },
                "Alt-Left": undefined,
                "Alt-Right": undefined,
                "Alt-Enter": undefined, 
                "Alt-Up": undefined,
                "Alt-Down": undefined,
                "Shift-Alt-Left": undefined,
                "Shift-Alt-Right": undefined,
                "Shift-Alt-Enter": undefined,
                "Shift-Alt-Up": undefined,
                "Shift-Alt-Down": undefined,
            });
        };

        var indentKey = 'indentUnit';
        var useTabsKey = 'indentWithTabs';
        var fontKey = 'fontSize';
        var spellcheckKey = 'spellcheck';
        var updateIndentSettings = editor.updateSettings = function () {
            if (!metadataMgr) { return; }
            var data = metadataMgr.getPrivateData().settings;
            data = data.codemirror || {};
            var indentUnit = data[indentKey];
            var useTabs = data[useTabsKey];
            var fontSize = data[fontKey];
            var spellcheck = data[spellcheckKey];
            var brackets = data.brackets;
            setIndentation(
                typeof(indentUnit) === 'number'? indentUnit : 2,
                typeof(useTabs) === 'boolean'? useTabs : false,
                typeof(fontSize) === 'number' ? fontSize : 12,
                typeof(spellcheck) === 'boolean' ? spellcheck : false,
                typeof(brackets) === 'boolean' ? brackets : true);
        };
        metadataMgr.onChangeLazy(updateIndentSettings);
        updateIndentSettings();
    };

    module.create = function (defaultMode, CMeditor, textarea) {
        var exp = {};

        var CodeMirror = exp.CodeMirror = CMeditor;
        CodeMirror.modeURL = "cm/mode/%N/%N";

        var $pad = $('#pad-iframe');
        var $textarea = exp.$textarea = textarea ? $(textarea) : $('#editor1');
        if (!$textarea.length) { $textarea = exp.$textarea = $pad.contents().find('#editor1'); }

        var onLocal = function () {};
        var $drawer;
        exp.init = function (local, title, toolbar) {
            if (typeof local === "function") {
                onLocal = local;
            }
            $drawer = toolbar.$theme || $();
        };

        var editor = exp.editor = CMeditor.fromTextArea($textarea[0], {
            allowDropFileTypes: [],
            lineNumbers: true,
            lineWrapping: true,
            autoCloseBrackets: true,
            matchBrackets : true,
            showTrailingSpace : true,
            styleActiveLine : true,
            search: true,
            inputStyle: 'contenteditable',
            highlightSelectionMatches: {showToken: /\w+/},
            extraKeys: {"Shift-Ctrl-R": undefined},
            foldGutter: true,
            gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
            mode: defaultMode || "javascript",
            readOnly: true
        });
        editor.focus();

        // Fix cursor and scroll position after undo/redo
        var undoData;
        editor.on('beforeChange', function (editor, change) {
            if (change.origin !== "undo" && change.origin !== "redo") { return; }
            undoData = editor.getValue();
        });
        editor.on('change', function (editor, change) {
            if (change.origin !== "undo" && change.origin !== "redo") { return; }
            if (typeof(undoData) === "undefined") { return; }
            var doc = editor.getValue();
            var ops = ChainPad.Diff.diff(undoData, doc);
            undoData = undefined;
            if (!ops.length) { return; }
            var cursor = posToCursor(ops[0].offset, doc);
            editor.setCursor(cursor);
            editor.scrollIntoView(cursor);
        });

        module.handleImagePaste(editor);

        var setMode = exp.setMode = function (mode, cb) {
            if (!mode) { return; }
            exp.highlightMode = mode;
            if (mode === 'markdown') { mode = 'gfm'; }
            if (/text\/x/.test(mode)) {
                CMeditor.autoLoadMode(editor, 'clike');
                editor.setOption('mode', mode);
            } else if (mode === 'asciidoc') {
                CMeditor.autoLoadMode(editor, mode, {
                    path: function () {
                        return 'cm-extra/asciidoc/asciidoc';
                    }
                });
                editor.setOption('mode', mode);
            } else {
                if (mode !== "text") {
                    CMeditor.autoLoadMode(editor, mode);
                }
                editor.setOption('mode', mode);
            }
            if (exp.$language) {
                var name = exp.$language.$menu.find('a[data-value="' + mode + '"]').text() || undefined;
                name = name ? Messages.languageButton + ' ('+name+')' : Messages.languageButton;
                exp.$language.setValue(mode, name);
                exp.$language.find('span.cp-language-text').text(name);
                exp.$language.find('span.cp-language-text').prepend('<i class="fa fa-chevron-right"></i>');
            }

                if (mode === "orgmode") {
                    if (CodeMirror.orgmode && typeof (CodeMirror.orgmode.init) === "function") {
                        CodeMirror.orgmode.init(editor);
                    }
                } else {
                    if (CodeMirror.orgmode && typeof (CodeMirror.orgmode.destroy) === "function") {
                        CodeMirror.orgmode.destroy(editor);
                    }
                }

            if(cb) { cb(mode); }
        };

        var setTheme = exp.setTheme = (function () {
            var path = '/common/theme/';

            var $head = $(window.document.head);

            var themeLoaded = exp.themeLoaded = function (theme) {
                return $head.find('link[href*="'+theme+'"]').length;
            };

            var loadTheme = exp.loadTheme = function (theme) {
                $head.append($('<link />', {
                    rel: 'stylesheet',
                    href: path + theme + '.css',
                }));
            };

            return function (theme, $select) {
                if (!theme) {
                    editor.setOption('theme', 'default');
                } else {
                    if (!themeLoaded(theme)) {
                        loadTheme(theme);
                    }
                    editor.setOption('theme', theme);
                }
                if ($select) {
                    var name = theme || undefined;
                    name = name ? Messages.themeButton + ' ('+theme+')' : Messages.themeButton;
                    $select.setValue(theme, name);
                    $select.find('span.cp-theme-text').text(name);
                    $select.find('span.cp-theme-text').prepend('<i class="fa fa-chevron-right"></i>');
                }
            };
        }());

        exp.getHeadingText = function () {
            return module.getHeadingText(editor);
        };

        exp.configureLanguage = function (Common, cb, onModeChanged) {
            var options = [];
            Modes.list.forEach(function (l) {
                options.push({
                    tag: 'a',
                    attributes: {
                        'data-value': l.mode,
                    },
                    content: [l.language] // Pretty name of the language value
                });
            });
            var dropdownConfig = {
                text: Messages.languageButton, // Button initial text
                options: options, // Entries displayed in the menu
                isSelect: true,
                isSubmenuOf: $drawer,
                feedback: 'CODE_LANGUAGE',
                common: Common
            };
            var $block = exp.$language = UIElements.createDropdown(dropdownConfig);
            $block.find('button').attr('title', Messages.languageButtonTitle).hide();
            $block.prepend(h('span.cp-language-text', Messages.languageButton));

            var isHovering = false;
            var $aLanguages = $block.$menu.find('li');
            $aLanguages.mouseenter(function () {
                isHovering = true;
                setMode($(this).find('a').attr('data-value'));
            });
            $aLanguages.mouseleave(function () {
                if (isHovering) {
                    setMode($block.$menu.find(".cp-dropdown-element-active").attr('data-value'));
                }
            });
            //$aLanguages.click(function () {
            $block.onChange.reg(() => {
                isHovering = false;
                var mode = $block.getValue();
                setMode(mode, onModeChanged);
                $block.close();
                onLocal();
            });

            if ($drawer) {
                var $blockButton = UIElements.createDropdownEntry({
                    tag: 'a',
                    content: $block[0],
                    action: function () {
                        $block.find('button').click();
                    },
                });
                dropdownConfig.$parentButton = $blockButton;
                $drawer.append($blockButton);
            }
            if (exp.highlightMode) { exp.setMode(exp.highlightMode); }
            if (cb) { cb(); }
        };

        exp.configureTheme = function (Common, cb) {
            /*  Remember the user's last choice of theme using localStorage */
            var isDark = window.CryptPad_theme === "dark";
            var themeKey = ['codemirror', isDark ? 'themedark' : 'theme'];
            var defaultTheme = isDark ? 'cryptpad-dark' : 'default';

            var todo = function (err, lastTheme) {
                lastTheme = lastTheme || defaultTheme;
                var options = [];
                Themes.forEach(function (l) {
                    options.push({
                        tag: 'a',
                        attributes: {
                            'data-value': l.name,
                        },
                        content: [l.name] // Pretty name of the language value
                    });
                });
                var dropdownConfig = {
                    text: Messages.code_editorTheme, // Button initial text
                    options: options, // Entries displayed in the menu
                    isSelect: true,
                    isSubmenuOf: $drawer,
                    initialValue: lastTheme,
                    feedback: 'CODE_THEME',
                    common: Common
                };
                var $block = exp.$theme = UIElements.createDropdown(dropdownConfig);
                /*$block.find('button').attr('title', Messages.themeButtonTitle).click(function () {
                    var state = $block.find('.cp-dropdown-content').is(':visible');
                    var $c = $block.closest('.cp-toolbar-drawer-content');
                    $c.removeClass('cp-dropdown-visible');
                    if (!state) {
                        $c.addClass('cp-dropdown-visible');
                    }
                });*/
                $block.find('button').hide();
                $block.prepend(h('span.cp-theme-text', Messages.languageButton));

                setTheme(lastTheme, $block);

                var isHovering = false;
                var $aThemes = $block.$menu.find('li');
                $aThemes.mouseenter(function () {
                    isHovering = true;
                    var theme = $(this).find('a').attr('data-value');
                    setTheme(theme, $block);
                });
                $aThemes.mouseleave(function () {
                    if (isHovering) {
                        setTheme(lastTheme, $block);
                        Common.setAttribute(themeKey, lastTheme);
                    }
                });
                $block.onChange.reg(() => {
                    isHovering = false;
                    var theme = $block.getValue();
                    setTheme(theme, $block);
                    Common.setAttribute(themeKey, theme);
                });

                if ($drawer) {
                    const $blockButton = UIElements.createDropdownEntry({
                        tag: 'a',
                        content: $block[0],
                        action: function () {
                            $block.find('button').click();
                        },
                    });
                    dropdownConfig.$parentButton = $blockButton;
                    $drawer.append($blockButton);
                }
                if (cb) { cb(); }
            };
            Common.getAttribute(themeKey, todo);
        };

        exp.getContentExtension = function () {
            return module.getContentExtension(exp.highlightMode);
        };
        exp.fileExporter = function () {
            return module.fileExporter(editor.getValue());
        };
        exp.fileImporter = function (content, file) {
            var $toolbarContainer = $('#cme_toolbox');
            var mime = CodeMirror.findModeByMIME(file.type);
            var mode;
            if (!mime) {
                var ext = /.+\.([^.]+)$/.exec(file.name);
                if (ext && ext[1]) {
                    mode = CMeditor.findModeByExtension(ext[1]);
                    mode = mode && mode.mode || null;
                }
            } else {
                mode = mime && mime.mode || null;
            }
            if (mode === "markdown") { mode = "gfm"; }
            if (mode && Modes.list.some(function (o) { return o.mode === mode; })) {
                exp.setMode(mode);
                $toolbarContainer.find('#language-mode').val(mode);
            } else {
                console.log("Couldn't find a suitable highlighting mode: %s", mode);
                exp.setMode('text');
                $toolbarContainer.find('#language-mode').val('text');
            }
            // return the mode so that the code editor can decide how to display the new content
            return { content: content, highlightMode: mode, authormarks: {} };
        };

        exp.setValueAndCursor = function (oldDoc, remoteDoc) {
            return module.setValueAndCursor(editor, oldDoc, remoteDoc);
        };

        /////

        var canonicalize = exp.canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); };


        exp.contentUpdate = function (newContent) {
            var oldDoc = canonicalize(editor.getValue());
            var remoteDoc = newContent.content;
            // setValueAndCursor triggers onLocal, even if we don't make any change to the content
            // and it may revert other changes (metadata)

            if (oldDoc === remoteDoc) { return; }
            exp.setValueAndCursor(oldDoc, remoteDoc);
        };

        exp.getContent = function () {
            editor.save();
            return { content: canonicalize(editor.getValue()) };
        };

        exp.mkIndentSettings = function (metadataMgr) {
            module.mkIndentSettings(editor, metadataMgr);
        };

        exp.getCursor = function () {
            var doc = canonicalize(editor.getValue());
            var cursor = {};
            cursor.selectionStart = cursorToPos(editor.getCursor('from'), doc);
            cursor.selectionEnd = cursorToPos(editor.getCursor('to'), doc);
            return cursor;
        };

        var makeCursor = function (id) {
            if (document.getElementById(id)) {
                return document.getElementById(id);
            }
            return $('<span>', {
                'id': id,
                'class': 'cp-codemirror-cursor'
            })[0];
        };
        var makeTippy = function (cursor) {
            return MT.getCursorAvatar(cursor);
        };
        var marks = {};
        exp.removeCursors = function () {
            for (var id in marks) {
                marks[id].clear();
                delete marks[id];
            }
        };
        exp.setRemoteCursor = function (data) {
            if (data.reset) {
                return void exp.removeCursors();
            }
            if (data.leave) {
                $('.cp-codemirror-cursor[id^='+data.id+']').each(function (i, el) {
                    var id = $(el).attr('id');
                    if (marks[id]) {
                        marks[id].clear();
                        delete marks[id];
                    }
                });
                return;
            }

            var id = data.id;
            var cursor = data.cursor;
            var doc = canonicalize(editor.getValue());

            if (marks[id]) {
                marks[id].clear();
                delete marks[id];
            }

            if (!cursor.selectionStart) { return; }

            if (cursor.selectionStart === cursor.selectionEnd) {
                var cursorPosS = posToCursor(cursor.selectionStart, doc);
                var el = makeCursor(id);
                if (cursor.color) {
                    $(el).css('border-color', cursor.color)
                         .css('background-color', cursor.color);
                }
                if (cursor.name) {
                    $(el).attr('title', makeTippy(cursor))
                         .attr('data-cptippy-html', true);
                }
                marks[id] = editor.setBookmark(cursorPosS, { widget: el });
            } else {
                var pos1 = posToCursor(cursor.selectionStart, doc);
                var pos2 = posToCursor(cursor.selectionEnd, doc);
                var css = cursor.color
                    ? 'background-color: rgba(' + Util.hexToRGB(cursor.color).join(',') + ',0.2)'
                    : 'background-color: rgba(255,0,0,0.2)';
                marks[id] = editor.markText(pos1, pos2, {
                    css: css,
                    attributes: {
                        'data-cptippy-html': true,
                    },
                    title: makeTippy(cursor),
                    className: 'cp-tippy-html'
                });
            }
        };

        return exp;
    };

    return module;
});

