var CodeOceanEditor = {
    //ACE-Editor-Path
    // ruby part adds the relative_url_root, if it is set.
    ACE_FILES_PATH: '<%= "#{Rails.application.config.relative_url_root.chomp('/')}/assets/ace/" %>',
    THEME: window.getCurrentTheme() === 'dark' ? 'ace/theme/tomorrow_night' : 'ace/theme/tomorrow',

    //Color-Encoding for Percentages in Progress Bars (For submissions)
    ADEQUATE_PERCENTAGE: 50,
    SUCCESSFULL_PERCENTAGE: 90,

    //Key-Codes (for Hotkeys)
    R_KEY_CODE: 82,
    S_KEY_CODE: 83,
    T_KEY_CODE: 84,
    ENTER_KEY_CODE: 13,

    //Request-For-Comments-Configuration
    REQUEST_FOR_COMMENTS_DELAY: 0,
    REQUEST_TOOLTIP_TIME: 5000,
    REQUEST_TOOLTIP_DELAY: 15 * 60 * 1000,

    editors: [],
    editor_for_file: new Map(),
    regex_for_language: new Map(),
    tracepositions_regex: undefined,

    active_file: undefined,
    active_frame: undefined,
    running: false,

    lastCopyText: null,
    lastDeltaObject_for_file: new Map(),

    <% self.class.include Rails.application.routes.url_helpers %>
    <% @config ||= CodeOcean::Config.new(:code_ocean).read(erb: false) %>
    // Important notice: Changing the config values requires any content-wise
    // modification for this file in the development environment. Lacking to do so
    // will result in the old, server-side cached serving of this file even across
    // server restarts!
    sendEvents: <%= @config['codeocean_events'] ? @config['codeocean_events']['enabled'] : false %>,
    eventURL: "<%= @config['codeocean_events'] ? events_path : '' %>",
    fileTypeURL: "<%= file_types_path %>",


    configureEditors: function () {
        _.each(['modePath', 'themePath', 'workerPath'], function (attribute) {
            ace.config.set(attribute, this.ACE_FILES_PATH);
        }.bind(this));
    },

    confirmDestroy: function (event) {
        event.preventDefault();
        if (confirm($(event.target).data('message-confirm'))) {
            this.destroyFile();
        }
    },

    confirmReset: function (event) {
        event.preventDefault();
        const initiator = $(event.target.closest("button"));
        if (confirm(initiator.data('message-confirm'))) {
            this.resetCode(initiator);
        }
    },

    confirmResetActiveFile: function (event) {
        event.preventDefault();
        const initiator = $(event.target.closest("button"));
        let message = initiator.data('message-confirm');
        message = message.replace('%{filename}', CodeOceanEditor.active_file.filename.replace(/#$/, ''))
        if (confirm(message)) {
            this.resetCode(initiator, true); // delete only active file
        }
    },

    fileActionsAvailable: function () {
        return this.isActiveFileRenderable() || this.isActiveFileRunnable() || this.isActiveFileStoppable() || this.isActiveFileTestable();
    },

    findOrCreateOutputElement: function (index) {
        if ($('#output-' + index).isPresent()) {
            return $('#output-' + index);
        } else {
            var element = $('<div class="mb-2 output-element">').attr('id', 'output-' + index);
            $('#output').append(element);
            return element;
        }
    },

    getCardClass: function (result) {
        if (result.file_role === 'teacher_defined_linter') {
            return 'info'
        } else if (result.stderr && !result.score) {
            return 'danger';
        } else if (result.score < 1) {
            return 'warning';
        } else {
            return 'success';
        }
    },

    showOutput: function (event) {
        const target = $(event.target).attr('href');
        if (target !== "#") {
            event.preventDefault();
            this.showOutputBar();
            $('body').scrollTo($(event.target).attr('href'));
        }
    },

    renderProgressBar: function (score, maximum_score) {
        var percentage = score / maximum_score * 100;
        var progress_bar = $('#score .progress-bar');
        progress_bar.removeClass().addClass(this.getProgressBarClass(percentage));
        progress_bar.attr({
            'aria-valuemax': maximum_score,
            'aria-valuemin': 0,
            'aria-valuenow': score
        });
        progress_bar.css('width', percentage + '%');
    },

    // The event ready.jstree is fired too early and thus doesn't work.
    selectFileInJsTree: function (filetree, file_id) {
        if (!filetree.is(':visible'))
            // The left sidebar is not shown and thus the filetree is not rendered.
            return;

        if (!filetree.hasClass('jstree-loading')) {
            filetree.jstree("deselect_all");
            filetree.jstree(true).select_node(file_id);
        } else {
            setTimeout(CodeOceanEditor.selectFileInJsTree.bind(null, filetree, file_id), 250);
        }
    },

    showFirstFile: function (own_solution = false) {
        let frame;
        let filetree;
        let editorSelector;
        if (own_solution) {
            frame = $('.own-frame[data-role="main_file"]').isPresent() ? $('.own-frame[data-role="main_file"]') : $('.own-frame').first();
            filetree = $('#own-files');
            editorSelector = '.own-editor';
        } else {
            frame = $('.frame[data-role="main_file"]').isPresent() ? $('.frame[data-role="main_file"]') : $('.frame').first();
            filetree = $('#files');
            editorSelector = '.editor';
        }

        var file_id = frame.find(editorSelector).data('file-id');
        this.setActiveFile(frame.data('filename'), file_id);
        this.selectFileInJsTree(filetree, file_id);
        this.showFrame(frame);
        this.toggleButtonStates();
    },

    showFrame: function (frame) {
        if (frame.hasClass('own-frame')) {
            $('.own-frame').hide();
        } else {
            $('.frame').hide();
        }

        this.active_frame = frame;
        frame.show();
        this.resizeParentOfAceEditor(frame.find('.ace_editor'));
    },

    getProgressBarClass: function (percentage) {
        if (percentage < this.ADEQUATE_PERCENTAGE) {
            return 'progress-bar progress-bar-striped bg-danger';
        } else if (percentage < this.SUCCESSFULL_PERCENTAGE) {
            return 'progress-bar progress-bar-striped bg-warning';
        } else {
            return 'progress-bar progress-bar-striped bg-success';
        }
    },

    handleKeyPress: function (event) {
        if (event.altKey && event.which === this.R_KEY_CODE) {
            $('#run').trigger('click');
        } else if (event.altKey && event.which === this.S_KEY_CODE) {
            $('#assess').trigger('click');
        } else if (event.altKey && event.which === this.T_KEY_CODE) {
            $('#test').trigger('click');
        } else {
            return;
        }
        event.preventDefault();
    },

    handleCopyEvent: function (text) {
        CodeOceanEditor.lastCopyText = text;
    },

    handlePasteEvent: function (pasteObject, event) {
        var same = (CodeOceanEditor.lastCopyText === pasteObject.text);

        // if the text is not copied from within the editor (from any file), send an event to the backend
        if (!same) {
            CodeOceanEditor.publishCodeOceanEvent({
                category: 'editor_paste',
                data: pasteObject.text,
                exercise_id: $('#editor').data('exercise-id'),
                file_id: $(event.container).data('file-id')
            });
        }
    },

    hideSpinner: function () {
        $('button i.fa-solid, button i.fa-regular').show();
        $('button i.fa-spin').removeClass('d-inline-block').addClass('d-none');
    },

    startSentryTransaction: function (initiator) {
        const cause = initiator.data('cause') || initiator.prop('id');
        this.sentryTransaction = window.SentryUtils.startIdleTransaction(
          Sentry.getCurrentHub(),
          { name: cause, op: "transaction" },
          0, // Idle Timeout
          window.SentryUtils.TRACING_DEFAULTS.finalTimeout,
          true); // onContext
        Sentry.getCurrentHub().configureScope(scope => scope.setSpan(this.sentryTransaction));
    },

    resizeAceEditors: function (own_solution = false) {
        let editorSelector;
        if (own_solution) {
            editorSelector = $('.own-editor')
        } else {
            editorSelector = $('.editor')
        }

        editorSelector.each(function (index, element) {
            this.resizeParentOfAceEditor(element);
        }.bind(this));
        window.dispatchEvent(new Event('resize'));
    },

    resizeSidebars: function () {
        $('#content-left-sidebar').height(this.calculateEditorHeight('#content-left-sidebar', false));
        $('#content-right-sidebar').height(this.calculateEditorHeight('#content-right-sidebar', false));
    },

    calculateEditorHeight: function (element, considerStatusbar) {
        const jqueryElement = $(element);
        if (jqueryElement.length === 0) {
            return 0;
        }

        const bottom = considerStatusbar ? ($('#statusbar').height() || 0) : 0;
        // calculate needed size: window height - position of top of ACE editor - height of autosave label below editor - 5 for bar margins
        return window.innerHeight - jqueryElement.offset().top - bottom - 5;
    },

    resizeParentOfAceEditor: function (element) {
        const editorHeight = this.calculateEditorHeight(element, true);
        $(element).parent().height(editorHeight);
    },

    initializeEditors: function (own_solution = false) {
        // Initialize the editors array if not present already. This is mainly required for community solutions
        this.editors = this.editors || [];
        let editorSelector;
        if (own_solution) {
            editorSelector = $('.own-editor')
        } else {
            editorSelector = $('.editor')
        }

        editorSelector.each(function (index, element) {

            // Resize frame on load
            this.resizeParentOfAceEditor(element);

            // Resize frame on window size change
            $(window).resize(function () {
                this.resizeParentOfAceEditor(element);
                this.resizeSidebars();
            }.bind(this));

            var editor = ace.edit(element);

            var document = editor.getSession().getDocument();
            // insert pre-existing code into editor. we have to use insertLines, otherwise the deltas are not properly added
            var file_id = $(element).data('file-id');
            var content = $('.editor-content[data-file-id=' + file_id + ']');
            this.setActiveFile($(element).parent().data('filename'), file_id);

            document.insertLines(0, content.text().split(/\n/));
            // remove last (empty) that is there by default line
            document.removeLines(document.getLength() - 1, document.getLength() - 1);
            editor.setReadOnly($(element).parent().data('read-only') !== undefined);
            if (editor.getReadOnly()) {
                editor.setHighlightActiveLine(false);
                editor.setHighlightGutterLine(false);
                editor.renderer.$cursorLayer.element.style.opacity = 0;
            }
            editor.setShowPrintMargin(false);
            editor.setTheme(this.THEME);


            // set options for autocompletion
            if ($(element).data('allow-auto-completion')) {
                editor.setOptions({
                    enableBasicAutocompletion: true,
                    enableSnippets: false,
                    enableLiveAutocompletion: true
                });
            }

            editor.commands.bindKey("ctrl+alt+0", null);
            this.editors.push(editor);
            this.editor_for_file.set($(element).parent().data('filename'), editor);
            this.lastDeltaObject_for_file.set($(element).parent().data('filename'), null);
            var session = editor.getSession();
            var mode = $(element).data('mode')
            session.setMode(mode);
            session.setTabSize($(element).data('indent-size'));
            session.setUseSoftTabs(true);
            session.setUseWrapMode(true);

            // set regex for parsing error traces based on the mode of the main file.
            if ($(element).parent().data('role') === "main_file") {
                this.tracepositions_regex = this.regex_for_language.get($(element).data('mode'));
            }

            /*
             * Register event handlers
             */

            // editor itself
            editor.on("paste", this.handlePasteEvent.bind(element));
            editor.on("copy", this.handleCopyEvent.bind(element));

            // listener for autosave
            session.on("change", function (deltaObject, session) {
                if (this.compareDeltaObjects(deltaObject, this.lastDeltaObject_for_file.get(this.active_file.filename))) {
                    this.lastDeltaObject_for_file.set(this.active_file.filename, null);
                    return;
                }
                App.synchronized_editor?.editor_change(deltaObject, this.active_file);

                // TODO: This is a workaround for a bug in Ace. Remove when upgrading Ace.
                this.handleUTF16Surrogates(deltaObject, session);
                this.resetSaveTimer();
            }.bind(this));
        }.bind(this));
    },

    handleAceThemeChangeEvent: function (event) {
        this.editors.forEach(function (editor) {
            editor.setTheme(this.THEME);
        }.bind(this));
    },

    handleUTF16Surrogates: function (AceDeltaObject, AceSession) {
        if (AceDeltaObject.data === undefined || AceDeltaObject.data.action !== "removeText") {
            return;
        }

        const codePoint = AceDeltaObject.data.text.codePointAt(0);
        if (0xDC00 <= codePoint && codePoint <= 0xDFFF) {
            // The text contains a UTF-16 surrogate pair, and the only the lower part is removed.
            // We need to remove the high surrogate pair as well.
            const currentCharacter = AceDeltaObject.data.range
            const previousCharacter = {
                start: {
                    row: currentCharacter.start.row,
                    column: currentCharacter.start.column - 1
                },
                end: {
                    row: currentCharacter.start.row,
                    column: currentCharacter.start.column
                }
            }
            AceSession.remove(previousCharacter);
        }
    },

    initializeEventHandlers: function () {
        $(document).on('click', '#results a', this.showOutput.bind(this));
        $(document).on('keydown', this.handleKeyPress.bind(this));
        $(document).on('theme:change:ace', this.handleAceThemeChangeEvent.bind(this));
        $('#start_chat').on('click', function(event) {
            this.createEventHandler('pp_start_chat', null)(event)
            // Allow to open the new tab even in Safari.
            // See: https://stackoverflow.com/a/70463940
            setTimeout(() => {
                var pop_up_window = window.open($('#start_chat').data('url'), '_blank');
                if (pop_up_window) {
                    pop_up_window.onerror = function (message) {
                        $.flash.danger({text: message});
                        this.sendError(message, null);
                    };
                }
            })
        }.bind(this));
        this.initializeFileTreeButtons();
        this.initializeWorkspaceButtons();
        this.initializeRequestForComments()
    },

    teardownEventHandlers: function () {
        $(document).unbind('click');
        $(document).unbind('keydown');
        this.teardownWorkspaceButtons();
        this.teardownRequestForComments();
        const rfcModal = $('#comment-modal');
        if (rfcModal.isPresent()) {
            bootstrap.Modal.getInstance(rfcModal)?.hide();
        }
        this.teardownFileTreeButtons();
    },

    updateEditorModeToFileTypeID: function (editor, fileTypeID) {
        var newMode = 'ace/mode/text'

        $.ajax(this.fileTypeURL + '/' + fileTypeID, {
            dataType: 'json'
        }).done(function (data) {
            if (data['editor_mode'] !== null) {
                newMode = data['editor_mode'];
            }
        }).fail(_.noop)
            .always(function () {
                ace.edit(editor).session.setMode(newMode);
            });
    },

    initializeFileTree: function (own_solution = false) {
        let filesInstance;
        if (own_solution) {
            filesInstance = $('#own-files');
        } else {
            filesInstance = $('#files');
        }
        const jsTreeConfig = filesInstance.data('entries') || {core: {}};
        jsTreeConfig.core.themes = {...jsTreeConfig.core.themes, name: window.getCurrentTheme() === "dark" ? "default-dark" : "default"}
        filesInstance.jstree(jsTreeConfig);
        filesInstance.on('click', 'li.jstree-leaf > a', function (event) {
            const file_id = parseInt($(event.target).parent().attr('id'));
            const frame = $('[data-file-id="' + file_id + '"]').parent();
            this.setActiveFile(frame.data('filename'), file_id);
            this.showFrame(frame);
            this.toggleButtonStates();
        }.bind(this));
        $(document).on('theme:change', function(event) {
            const jsTree = filesInstance?.jstree(true);

            if (jsTree) {
                const newColorScheme = event.detail.currentTheme;
                // Update the JStree theme
                jsTree?.set_theme(newColorScheme === "dark" ? "default-dark" : "default");
            }
        });
    },

    initializeFileTreeButtons: function () {
        $('#create-file').on('click', this.showFileDialog.bind(this));
        $('#destroy-file').on('click', this.confirmDestroy.bind(this));
        $('#destroy-file-collapsed').on('click', this.confirmDestroy.bind(this));
        $('#download').on('click', this.downloadCode.bind(this));
    },

    teardownFileTreeButtons: function () {
        $('#create-file').unbind('click');
        $('#destroy-file').unbind('click');
        $('#destroy-file-collapsed').unbind('click');
        $('#download').unbind('click');
    },

    initializeSideBarCollapse: function () {
        $('#sidebar-collapse-collapsed').on('click', this.handleSideBarToggle.bind(this));
        $('#sidebar-collapse').on('click', this.handleSideBarToggle.bind(this));
        const tipButton = $('#tips-collapsed');
        if (tipButton) {
            tipButton.on('click', this.handleSideBarToggle.bind(this));
        }
        $('#sidebar').on('transitionend', this.resizeAceEditors.bind(this));
        $('#sidebar').on('transitionend', this.resizeSidebars.bind(this));
    },

    handleSideBarToggle: function () {
        const sidebar = $('#sidebar');
        sidebar.toggleClass('sidebar-col').toggleClass('sidebar-col-collapsed');
        if (sidebar.hasClass('w-25') || sidebar.hasClass('restore-to-w-25')) {
            sidebar.toggleClass('w-25').toggleClass('restore-to-w-25');
        }
        $('#sidebar-collapsed').toggleClass('d-none');
        $('#sidebar-uncollapsed').toggleClass('d-none');
    },

    initializeRegexes: function () {
        // These RegEx are run on the HTML escaped output!
        this.regex_for_language.set("ace/mode/python", /File &quot;(.+?)&quot;, line (\d+)/g);
        this.regex_for_language.set("ace/mode/java", /(?:\.\/)?(.*\.java):(\d+):/g);
    },

    initializeWorkspaceButtons: function () {
        $('#submit').one('click', this.submitCode.bind(this));
        $('#assess').on('click', this.scoreCode.bind(this));
        $('#render').on('click', this.renderCode.bind(this));
        $('#run').on('click', this.runCode.bind(this));
        $('#stop').on('click', this.stopCode.bind(this));
        $('#test').on('click', this.testCode.bind(this));
        $('#start-over').on('click', this.confirmReset.bind(this));
        $('#start-over-active-file').on('click', this.confirmResetActiveFile.bind(this));
    },

    teardownWorkspaceButtons: function () {
        $('#submit').unbind('click');
        $('#assess').unbind('click');
        $('#render').unbind('click');
        $('#run').unbind('click');
        $('#stop').unbind('click');
        $('#test').unbind('click');
        $('#start-over').unbind('click');
        $('#start-over-active-file').unbind('click');
    },

    initializeRequestForComments: function () {
        var button = $('#requestComments');
        button.prop('disabled', true);
        button.on('click', function () {
            button.tooltip('hide');
            $('#rfc_intervention_text').hide()
            new bootstrap.Modal($('#comment-modal')).show();
        });

        $('#askForCommentsButton').one('click', this.requestComments.bind(this));
        $('#closeAskForCommentsButton').on('click', function () {
            bootstrap.Modal.getInstance($('#comment-modal')).hide();
        });

        setTimeout(function () {
            button.prop('disabled', false);
            setTimeout(function () {
                button.tooltip('show');
                setTimeout(function () {
                    button.tooltip('hide');
                }, this.REQUEST_TOOLTIP_TIME);
            }, this.REQUEST_TOOLTIP_DELAY)
        }.bind(this), this.REQUEST_FOR_COMMENTS_DELAY);
    },

    teardownRequestForComments: function () {
        $('#requestComments').unbind('click');
        $('#askForCommentsButton').unbind('click');
        $('#closeAskForCommentsButton').unbind('click');
    },

    isActiveFileRenderable: function () {
        if (this.active_frame.data() === undefined) {
            return false;
        }
        return 'renderable' in this.active_frame.data();
    },

    isActiveFileRunnable: function () {
        return this.isActiveFileExecutable() && ['main_file', 'user_defined_file', 'executable_file'].includes(this.active_frame.data('role'));
    },

    isActiveFileStoppable: function () {
        return this.isActiveFileRunnable() && this.running;
    },

    isActiveFileSubmission: function () {
        return ['Submission'].includes(this.active_frame.data('contextType'));
    },

    isActiveFileTestable: function () {
        return this.isActiveFileExecutable() && ['teacher_defined_test', 'user_defined_test', 'teacher_defined_linter'].includes(this.active_frame.data('role'));
    },

    populateCard: function (card, result, index) {
        card.addClass(`card border-${this.getCardClass(result)}`);
        card.find('.card-header').addClass(`bg-${this.getCardClass(result)} text-white`);
        card.find('.card-title .filename').text(result.filename);
        card.find('.card-title .number').text(index + 1);
        card.find('.row .col-md-9').eq(0).find('.number').eq(0).text(result.passed);
        card.find('.row .col-md-9').eq(0).find('.number').eq(1).text(result.count);
        if (result.weight !== 0) {
            card.find('.row .col-md-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2)));
            card.find('.row .col-md-9').eq(1).find('.number').eq(1).text(result.weight);
        } else {
            // Hide score row if no score could be achieved
            card.find('.attribute-row.row').eq(1).addClass('d-none');
        }
        card.find('.row .col-md-9').eq(2).html(result.message);

        // Add error message from code to card
        if (result.error_messages) {
            const targetNode = card.find('.row .col-md-9').eq(3);

            let errorMessagesToShow = [];
            result.error_messages.forEach(function (item) {
                if (item) {
                    errorMessagesToShow.push(item)
                }
            })

            // delete all current elements
            targetNode.text('');
            // create a new list and append each element
            const ul = document.createElement("ul");

            // Extract detailed linter results
            if (result.file_role === 'teacher_defined_linter') {
                const detailed_linter_results = result.detailed_linter_results;
                const severity_groups = detailed_linter_results.reduce(function(map, obj) {
                    map[obj.severity] = map[obj.severity] || []
                    map[obj.severity].push(obj);
                    return map;
                }, {});

                for (let severity in severity_groups) {
                    if (!severity_groups.hasOwnProperty(severity)) {
                        continue;
                    }
                    const linter_results = severity_groups[severity]

                    const li = document.createElement("li");
                    const text = $.parseHTML(`<u>${severity}:</u>`);
                    $(li).append(text);
                    ul.append(li);

                    const sub_ul = document.createElement("ul");
                    sub_ul.setAttribute('class', 'inline_list');
                    for (let check_run of linter_results) {
                        const sub_li = document.createElement("li");

                        let scope = '';
                        if (check_run.scope) {
                             scope = `, ${check_run.scope}()`;
                        }
                        const context = `${check_run.file_name}: ${check_run.line}${scope}`;
                        const line_link = `<a href='#' data-file='${check_run.file_name}' data-line='${check_run.line}'>${context}</a>`;
                        const message = `${check_run.name}: ${check_run.result} (${line_link})`;
                        const sub_text = $.parseHTML(message);
                        $(sub_li).append(sub_text).on("click", "a", this.jumpToSourceLine.bind(this));
                        sub_ul.append(sub_li);
                    }
                    li.append(sub_ul);
                }

            // Just show standard results for normal test results
            } else {
              errorMessagesToShow.forEach(function (item) {
                  var li = document.createElement("li");
                  var text = document.createTextNode(item);
                  $(li).append(text);
                  ul.append(li);
              })
            }

            // one or more errors?
            if (errorMessagesToShow.length > 1) {
                ul.setAttribute('class', 'inline_list');
            } else {
                ul.setAttribute('class', 'single_entry_inline_list');
            }
            targetNode.append(ul);
        }
        //card.find('.row .col-md-9').eq(4).find('a').attr('href', '#output-' + index);
    },

    createEventHandler: function (eventType, data) {
        return function (event) {
            CodeOceanEditor.publishCodeOceanEvent({
                category: eventType,
                data: data,
                exercise_id: $('#editor').data('exercise-id'),
                file_id: CodeOceanEditor.active_file.id,
            });
            event.stopPropagation();
        };
    },

    publishCodeOceanEvent: function (payload) {
        if (this.sendEvents) {
            $.ajax(this.eventURL, {
                type: 'POST',
                cache: false,
                dataType: 'JSON',
                data: {
                    event: payload
                },
                success: _.noop,
                error: _.noop
            });
        }
    },

    sendError: function (message, submission_id) {
        this.showSpinner($('#render'));
        var jqxhr = this.ajax({
            data: {
                error: {
                    message: message,
                    submission_id: submission_id
                }
            },
            url: $('#editor').data('errors-url')
        });
        jqxhr.always(this.hideSpinner);
    },

    toggleButtonStates: function () {
        $('#destroy-file').prop('disabled', this.active_frame.data('role') !== 'user_defined_file');
        $('#start-over-active-file').prop('disabled', this.active_frame.data('role') === 'user_defined_file' || this.active_frame.data('read-only') !== undefined);
        $('#dummy').toggle(!this.fileActionsAvailable());
        $('#render').toggle(this.isActiveFileRenderable());
        $('#run-stop-button-group').tooltip('hide').toggleClass('flex-grow-1', this.isActiveFileRunnable() || this.isActiveFileStoppable());
        $('#run').toggle(this.isActiveFileRunnable() && !this.running);
        $('#stop').toggle(this.isActiveFileStoppable());
        $('#test').toggle(this.isActiveFileTestable());
    },

    jumpToSourceLine: function (event) {
        const file = $(event.target).data('file');
        const line = $(event.target).data('line');

        const frame = $('div.frame[data-filename="' + file + '"]');
        this.showFrame(frame);
        this.toggleButtonStates();

        const file_id = frame.find('.editor').data('file-id');
        this.setActiveFile(frame.data('filename'), file_id);
        this.selectFileInJsTree($('#files'), file_id);

        const editor = this.editor_for_file.get(file);
        editor.gotoLine(line, 0);
        event.preventDefault();
    },

    augmentStacktraceInOutput: function () {
        if (this.tracepositions_regex) {
            $('#output > .output-element').each($.proxy(function(index, element) {
                element = $(element)

                const text = _.escape(element.text());
                element.on("click", "a", this.jumpToSourceLine.bind(this));

                let matches;

                let augmented_text = element.html();
                while (matches = this.tracepositions_regex.exec(text)) {
                    const frame = $('div.frame[data-filename="' + matches[1] + '"]')

                    if (frame.length > 0) {
                        augmented_text = augmented_text.replace(new RegExp(_.unescape(matches[0]), 'g'), "<a href='#' data-file='" + matches[1] + "' data-line='" + matches[2] + "'>" + matches[0] + "</a>");
                    }
                }
                element.html(augmented_text);
            }, this));
        }
    },

    resetOutputTab: function () {
        this.clearOutput();
        $('#flowrHint').fadeOut();
        this.clearHints();
        this.showOutputBar();
        this.clearFileDownloads();
    },

    isActiveFileBinary: function () {
        if (this.active_frame.data() === undefined) {
            return false;
        }
        return 'binary' in this.active_frame.data();
    },

    isActiveFileExecutable: function () {
        if (this.active_frame.data() === undefined) {
            return false;
        }
        return 'executable' in this.active_frame.data();
    },

    setActiveFile: function (filename, fileId) {
        this.active_file = {
            filename: filename,
            id: fileId
        };
    },

    showSpinner: function (initiator) {
        $(initiator).closest('[data-bs-toggle="tooltip"]').tooltip('hide');
        $(initiator).find('i.fa-solid, i.fa-regular').hide();
        $(initiator).find('i.fa-spin').addClass('d-inline-block').removeClass('d-none');
    },

    showStatus: function (output) {
        if (output.status === 'timeout' || output.status === 'buffer_overflow') {
            this.showTimeoutMessage();
        } else if (output.status === 'container_depleted') {
            this.showContainerDepletedMessage();
        } else if (output.status === 'out_of_memory') {
            this.showOutOfMemoryMessage();
        }
    },

    clearHints: function () {
        var container = $('#error-hints');
        container.find('ul.body > li.hint').remove();
        container.fadeOut();
    },

    showHint: function (message) {
        var template = function (description, hint) {
            return '\
           <li class="hint">\
             <div class="description">\
               ' + description + '\
             </div>\
             <div class="hint">\
               ' + hint + '\
             </div>\
           </li>\
         '
        };
        var container = $('#error-hints');
        container.find('ul.body').append(template(message.description, message.hint));
        container.fadeIn();
    },

    prepareFileDownloads: function(message) {
        const fileTree = $('#download-file-tree');
        fileTree.jstree(message.data);
        fileTree.on('select_node.jstree', function (node, selected, _event) {
            selected.instance.deselect_all();
            const downloadPath = selected.node.original.download_path;
            if (downloadPath) {
                window.location = downloadPath;
            }
        }.bind(this));
        $('#download-files').removeClass('d-none');
    },

    clearFileDownloads: function() {
        $('#download-files').addClass('d-none');
        $('#download-file-tree').replaceWith($('<div id="download-file-tree">'));
    },

    showContainerDepletedMessage: function () {
        $.flash.danger({
            icon: ['fa-regular', 'fa-clock'],
            text: $('#editor').data('message-depleted')
        });
    },

    showOutOfMemoryMessage: function () {
        $.flash.info({
            icon: ['fa-regular', 'fa-clock'],
            text: $('#editor').data('message-out-of-memory')
        });
    },

    showTimeoutMessage: function () {
        $.flash.info({
            icon: ['fa-regular', 'fa-clock'],
            text: $('#editor').data('message-timeout')
        });
    },

    showWebsocketError: function (error) {
        if (window.navigator.userAgent.indexOf('Edge') > -1 || window.navigator.userAgent.indexOf('Trident') > -1) {
            // Mute errors in Microsoft Edge and Internet Explorer
            return;
        }
        $.flash.danger({
            text: $('#flash').data('websocket-failure'),
            showPermanent: true
        });
        Sentry.captureException(JSON.stringify(error, ["message", "arguments", "type", "name", "data"]));
    },

    showFileDialog: function (event) {
        event.preventDefault();
        this.createSubmission('#create-file', null, function (response) {
            $('#code_ocean_file_context_id').val(response.id);
            new bootstrap.Modal($('#modal-file')).show();
        }.bind(this));
    },

    initializeOutputBarToggle: function () {
        $('#toggle-sidebar-output').on('click', this.hideOutputBar.bind(this));
        $('#toggle-sidebar-output-collapsed').on('click', this.showOutputBar.bind(this));
        $('#output_sidebar').on('transitionend', this.resizeAceEditors.bind(this));
        $('#output_sidebar').on('transitionend', this.resizeSidebars.bind(this));
    },

    showOutputBar: function () {
        $('#output_sidebar_collapsed').addClass('d-none');
        $('#output_sidebar_uncollapsed').removeClass('d-none');
        $('#output_sidebar').removeClass('output-col-collapsed').addClass('output-col');
    },

    hideOutputBar: function () {
        $('#output_sidebar_collapsed').removeClass('d-none');
        $('#output_sidebar_uncollapsed').addClass('d-none');
        $('#output_sidebar').removeClass('output-col').addClass('output-col-collapsed');
    },

    initializeSideBarTooltips: function () {
        $('[data-bs-toggle="tooltip"]').tooltip()
    },

    initializeDescriptionToggle: function () {
        $('#exercise-headline').on('click', this.toggleDescriptionCard.bind(this));
        $('a#toggle').on('click', this.toggleDescriptionCard.bind(this));
    },

    toggleDescriptionCard: function (event) {
        $('#description-card').toggleClass('description-card-collapsed').toggleClass('description-card');
        $('#description-symbol').toggleClass('fa-chevron-down').toggleClass('fa-chevron-right');
        var toggle = $('a#toggle');
        toggle.text(toggle.text() == toggle.data('hide') ? toggle.data('show') : toggle.data('hide'));
        this.resizeAceEditors();
        this.resizeSidebars();
        event.preventDefault();
    },

    /**
     * interventions
     * */
    initializeInterventionTimer: function () {
        const editor = $('#editor');

        if (editor.data('rfc-interventions') || editor.data('break-interventions') || editor.data('tips-interventions')) { // split in break or rfc intervention
            window.onblur = function () {
                window.blurred = true;
            };
            window.onfocus = function () {
                window.blurred = false;
            };

            const delta = 100; // time in ms to wait for window event before time gets stopped
            let tid;
            $.ajax({
                dataType: 'json',
                method: 'GET',
                // get working times for this exercise
                url: editor.data('working-times-url'),
                success: function (data) {
                    const percentile75 = data['working_time_75_percentile'];
                    const accumulatedWorkTimeUser = data['working_time_accumulated'];

                    let minTimeIntervention = 20 * 60 * 1000;

                    let timeUntilIntervention;
                    if ((accumulatedWorkTimeUser - percentile75) > 0) {
                        // working time is already over 75 percentile
                        timeUntilIntervention = minTimeIntervention;
                    } else {
                        // working time is less than 75 percentile
                        // ensure we give user at least minTimeIntervention before we bother the user
                        timeUntilIntervention = Math.max(percentile75 - accumulatedWorkTimeUser, minTimeIntervention);
                    }

                    tid = setInterval(function () {
                        if (window.blurred) {
                            return;
                        }
                        timeUntilIntervention -= delta;
                        if (timeUntilIntervention <= 0) {
                            const interventionSaveUrl = editor.data('intervention-save-url');
                            clearInterval(tid);
                            // timeUntilIntervention passed
                            if (editor.data('tips-interventions')) {
                                const modal = $('#tips-intervention-modal');
                                if (!modal.isPresent()) {
                                    // The modal is not present (e.g., because the site was navigated), so we don't continue here.
                                    return;
                                }

                                modal.find('.modal-footer').html(I18n.t("exercises.implement.intervention.explanation", {duration: Math.round(percentile75 / 60)}));
                                new bootstrap.Modal(modal).show();
                                $.ajax({
                                    data: {
                                        intervention_type: 'TipsIntervention'
                                    },
                                    dataType: 'json',
                                    type: 'POST',
                                    url: interventionSaveUrl
                                });
                            } else if (editor.data('break-interventions')) {
                                const modal = $('#break-intervention-modal');
                                if (!modal.isPresent()) {
                                    // The modal is not present (e.g., because the site was navigated), so we don't continue here.
                                    return;
                                }

                                modal.find('.modal-footer').html(I18n.t("exercises.implement.intervention.explanation", {duration: Math.round(percentile75 / 60)}));
                                new bootstrap.Modal(modal).show();
                                $.ajax({
                                    data: {
                                        intervention_type: 'BreakIntervention'
                                    },
                                    dataType: 'json',
                                    type: 'POST',
                                    url: interventionSaveUrl
                                });
                            } else if (editor.data('rfc-interventions')) {
                                const button = $('#requestComments');
                                // only show intervention if user did not requested for a comment already
                                if (!button.prop('disabled')) {
                                    $('#rfc_intervention_text').show();
                                    const modal = $('#comment-modal');
                                    if (!modal.isPresent()) {
                                        // The modal is not present (e.g., because the site was navigated), so we don't continue here.
                                        return;
                                    }

                                    modal.find('.modal-footer').html(I18n.t("exercises.implement.intervention.explanation", {duration: Math.round(percentile75 / 60)}));
                                    modal.on('hidden.bs.modal', function () {
                                        modal.find('.modal-footer').text('');
                                    });
                                    new bootstrap.Modal(modal).show();
                                    $.ajax({
                                        data: {
                                            intervention_type: 'QuestionIntervention'
                                        },
                                        dataType: 'json',
                                        type: 'POST',
                                        url: interventionSaveUrl
                                    });
                                }
                            }
                        }
                    }, delta);
                }
            });
        }
    },

    applyChanges: function (delta, active_file) {
        this.lastDeltaObject_for_file.set(active_file.filename, delta);
        const editor = this.editor_for_file.get(active_file.filename)
        editor.session.doc.applyDeltas([delta]);
    },

    compareDeltaObjects: function (delta, last_delta) {
        if (delta === null || last_delta === null) {
            return false;
        }

        const delta_data = delta.data
        return !_.isEqual(delta_data, last_delta);
    },

    showPartnersConnectionStatus: function (status, username) {
        switch(status) {
            case 'connected':
                $('#pg_session').text(I18n.t('exercises.editor.is_online', {name: username}));
                break;
            case 'disconnected':
                $('#pg_session').text(I18n.t('exercises.editor.is_offline', {name: username}));
                break;
        }
    },

    initializeEverything: function () {
        CodeOceanEditor.editors = [];
        this.initializeRegexes();
        this.configureEditors();
        this.initializeEditors();
        this.initializeEventHandlers();
        this.initializeFileTree();
        this.initializeSideBarCollapse();
        this.initializeOutputBarToggle();
        this.initializeDescriptionToggle();
        this.initializeSideBarTooltips();
        this.initializeInterventionTimer();
        this.initPrompt();
        this.renderScore();
        this.showFirstFile();
        this.resizeAceEditors();
        this.resizeSidebars();
        this.initializeDeadlines();
        CodeOceanEditorTips.initializeEventHandlers();

        window.addEventListener("turbolinks:before-render", App.synchronized_editor?.disconnect.bind(App.synchronized_editor));
        window.addEventListener("beforeunload", App.synchronized_editor?.disconnect.bind(App.synchronized_editor));

        window.addEventListener("turbolinks:before-render", this.autosaveIfChanged.bind(this));
        window.addEventListener("beforeunload", this.autosaveIfChanged.bind(this));
        // create autosave when the editor is opened the first time
        this.autosave();
    }
};
