$(function() { var ACE_FILES_PATH = '/assets/ace/'; var ADEQUATE_PERCENTAGE = 50; var ALT_1_KEY_CODE = 161; var ALT_2_KEY_CODE = 8220; var ALT_3_KEY_CODE = 182; var ALT_4_KEY_CODE = 162; var ALT_R_KEY_CODE = 174; var ALT_S_KEY_CODE = 8218; var ALT_T_KEY_CODE = 8224; var FILENAME_URL_PLACEHOLDER = '{filename}'; var SUCCESSFULL_PERCENTAGE = 90; var THEME = 'ace/theme/textmate'; var REMEMBER_TAB = false; var AUTOSAVE_INTERVAL = 15 * 1000; var NONE = 0; var WEBSOCKET = 1; var SERVER_SEND_EVENT = 2; var editors = []; var active_file = undefined; var active_frame = undefined; var running = false; var qa_api = undefined; var output_mode_is_streaming = true; var runmode = NONE; var websocket, turtlescreen, numMessages = 0, turtlecanvas = $('#turtlecanvas'), prompt = $('#prompt'), commands = ['input', 'write', 'turtle', 'turtlebatch', 'exit', 'status'], streams = ['stdin', 'stdout', 'stderr']; var ENTER_KEY_CODE = 13; var flowrResultHtml = '
' var ajax = function(options) { return $.ajax(_.extend({ dataType: 'json', method: 'POST', }, options)); }; var ajaxError = function(response) { var message = ((response || {}).responseJSON || {}).message || ''; $.flash.danger({ text: message.length > 0 ? message : $('#flash').data('message-failure') }); }; var clearOutput = function() { $('#output pre').remove(); }; var closeEventSource = function(event) { event.target.close(); hideSpinner(); running = false; toggleButtonStates(); if (event.type === 'error' || JSON.parse(event.data).code !== 200) { ajaxError(); showTab(0); } }; var collectFiles = function() { var editable_editors = _.filter(editors, function(editor) { return !editor.getReadOnly(); }); return _.map(editable_editors, function(editor) { return { content: editor.getValue(), file_id: $(editor.container).data('file-id') }; }); }; var configureEditors = function() { _.each(['modePath', 'themePath', 'workerPath'], function(attribute) { ace.config.set(attribute, ACE_FILES_PATH); }); }; var confirmDestroy = function(event) { event.preventDefault(); if (confirm($(this).data('message-confirm'))) { destroyFile(); } }; var confirmReset = function(event) { event.preventDefault(); if (confirm($(this).data('message-confirm'))) { resetCode(); } }; var confirmSubmission = function(event) { event.preventDefault(); if (confirm($(this).data('message-confirm'))) { submitCode(); } }; var createSubmission = function(initiator, filter, callback) { showSpinner(initiator); var annotations_arr = []; $('.editor').each(function(index, element) { var editor = ace.edit(element); var cleaned_annotations = editor.getSession().getAnnotations(); for(var i = cleaned_annotations.length-1; i>=0; --i){ cleaned_annotations[i].text = cleaned_annotations[i].text.replace(cleaned_annotations[i].username + ": ", ""); } annotations_arr = annotations_arr.concat(editor.getSession().getAnnotations()); }); var jqxhr = ajax({ data: { submission: { cause: $(initiator).data('cause') || $(initiator).prop('id'), exercise_id: $('#editor').data('exercise-id'), files_attributes: (filter || _.identity)(collectFiles()) }, annotations_arr: annotations_arr }, dataType: 'json', method: 'POST', url: $(initiator).data('url') || $('#editor').data('submissions-url') }); jqxhr.always(hideSpinner); jqxhr.done(createSubmissionCallback); jqxhr.done(callback); jqxhr.fail(ajaxError); }; var createSubmissionCallback = function(data){ // set all frames context types to submission $('.frame').each(function(index, element) { $(element).data('context-type', 'Submission'); }); // update the ids of the editors and reload the annotations for (var i = 0; i < editors.length; i++) { // set the data attribute to submission //$(editors[i].container).data('context-type', 'Submission'); var file_id_old = $(editors[i].container).data('file-id'); // file_id_old is always set. Either it is a reference to a teacher supplied given file, or it is the actual id of a new user created file. // This is the case, since it is set via a call to ancestor_id on the model, which returns either file_id if set, or id if it is not set. // therefore the else part is not needed any longer... // if we have an file_id set (the file is a copy of a teacher supplied given file) if (file_id_old != null){ // if we find file_id_old (this is the reference to the base file) in the submission, this is the match for(var j = 0; j< data.files.length; j++){ if(data.files[j].file_id == file_id_old){ //$(editors[i].container).data('id') = data.files[j].id; $(editors[i].container).data('id', data.files[j].id ); } } } setAnnotations(editors[i], $(editors[i].container).data('id')); } // toggle button states (it might be the case that the request for comments button has to be enabled toggleButtonStates(); }; var destroyFile = function() { createSubmission($('#destroy-file'), function(files) { return _.reject(files, function(file) { return file.file_id === active_file.id; }); }, window.CodeOcean.refresh); }; var downloadCode = function(event) { event.preventDefault(); createSubmission(this, null,function(response) { var url = response.download_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename); window.location = url; }); }; var evaluateCode = function(url, streamed, callback) { (streamed ? evaluateCodeWithStreamedResponse : evaluateCodeWithoutStreamedResponse)(url, callback); }; var evaluateCodeWithStreamedResponse = function(url, callback) { initWebsocketConnection(url); // TODO only init turtle when required initTurtle(); // TODO reimplement via websocket messsages /*var event_source = new EventSource(url); event_source.addEventListener('hint', renderHint); event_source.addEventListener('info', storeContainerInformation); if ($('#flowrHint').isPresent()) { event_source.addEventListener('output', handleStderrOutputForFlowr); event_source.addEventListener('close', handleStderrOutputForFlowr); } if (qa_api) { event_source.addEventListener('close', handleStreamedResponseForCodePilot); }*/ }; var handleStreamedResponseForCodePilot = function(event) { qa_api.executeCommand('syncOutput', [chunkBuffer]); chunkBuffer = [{streamedResponse: true}]; } var evaluateCodeWithoutStreamedResponse = function(url, callback) { var jqxhr = ajax({ method: 'GET', url: url }); jqxhr.always(hideSpinner); jqxhr.done(callback); jqxhr.fail(ajaxError); }; var fileActionsAvailable = function() { return isActiveFileRenderable() || isActiveFileRunnable() || isActiveFileStoppable() || isActiveFileTestable(); }; var findOrCreateOutputElement = function(index) { if ($('#output-' + index).isPresent()) { return $('#output-' + index); } else { var element = $('
').attr('id', 'output-' + index);
      $('#output').append(element);
      return element;
    }
  };

  var getPanelClass = function(result) {
    if (result.stderr && !result.score) {
      return 'panel-danger';
    } else if (result.score < 1) {
      return 'panel-warning';
    } else {
      return 'panel-success';
    }
  };

  var getProgressBarClass = function(percentage) {
    if (percentage < ADEQUATE_PERCENTAGE) {
      return 'progress-bar progress-bar-striped progress-bar-danger';
    } else if (percentage < SUCCESSFULL_PERCENTAGE) {
      return 'progress-bar progress-bar-striped progress-bar-warning';
    } else {
      return 'progress-bar progress-bar-striped progress-bar-success';
    }
  };

  var handleKeyPress = function(event) {
    if (event.which === ALT_1_KEY_CODE) {
      showWorkspaceTab(event);
    } else if (event.which === ALT_2_KEY_CODE) {
      showTab(1);
    } else if (event.which === ALT_3_KEY_CODE) {
      showTab(2);
    } else if (event.which === ALT_R_KEY_CODE) {
      $('#run').trigger('click');
    } else if (event.which === ALT_S_KEY_CODE) {
      $('#assess').trigger('click');
    } else if (event.which === ALT_T_KEY_CODE) {
      $('#test').trigger('click');
    } else {
      return;
    }
    event.preventDefault();
  };

  var lastCopyText;
  var handleCopyEvent = function(text){
    lastCopyText = text;
  };

  var handlePasteEvent = function (pasteObject) {
    //console.log("Handling paste event. this is ", this );
    //console.log("Text: " + pasteObject.text);

    var same = (lastCopyText === pasteObject.text);
    //console.log("Text is the same: " + same);

    // if the text is not copied from within the editor (from any file), send an event to lanalytics
    //if(!same){
    //  publishCodeOceanEvent("codeocean_editor_paste", {
    //    text: pasteObject.text,
    //    exercise: $('#editor').data('exercise-id'),
    //    file_id: "1"
    //
    //  });
    //}
  };

  var handleScoringResponse = function(response) {
    printScoringResults(response);
    var score = _.reduce(response, function(sum, result) {
      return sum + result.score * result.weight;
    }, 0).toFixed(2);
    $('#score').data('score', score);
    renderScore();
    showTab(2);
  };

  var stderrOutput = '';
  // activate flowr only for half of the audience
  var isFlowrEnabled = parseInt($('#editor').data('user-id'))%2 == 0;
  var handleStderrOutputForFlowr = function(event) {
  if (!isFlowrEnabled) return
    var json = JSON.parse(event.data);

    if (json.stderr) {
      stderrOutput += json.stderr;
    } else if (json.code) {
      if (stderrOutput == '') {
        return;
      }

      var flowrUrl = $('#flowrHint').data('url');
      var flowrHintBody = $('#flowrHint .panel-body');
      var queryParameters = {
        query: stderrOutput
      }

      flowrHintBody.empty();

      jQuery.getJSON(flowrUrl, queryParameters, function(data) {
        for (var question in data.queryResults) {
          var collapsibleTileHtml = flowrResultHtml.replace(/{{collapseId}}/g, 'collapse-' + question).replace(/{{headingId}}/g, 'heading-' + question);
          var resultTile = $(collapsibleTileHtml);

          resultTile.find('h4 > a').text(data.queryResults[question].title + ' | Found via ' + data.queryResults[question].source);
          resultTile.find('.panel-body').html(data.queryResults[question].body);
          resultTile.find('.panel-body').append('Open this question');

          flowrHintBody.append(resultTile);
        }

        if (data.queryResults.length !== 0) {
          $('#flowrHint').fadeIn();
        }
      })

      stderrOutput = '';
    }
  };

  var handleTestResponse = function(response) {
    clearOutput();
    printOutput(response[0], false, 0);
    if (qa_api) {
      qa_api.executeCommand('syncOutput', [response]);
    }
    showStatus(response[0]);
    showTab(1);
  };

  var hideSpinner = function() {
    $('button i.fa').show();
    $('button i.fa-spin').hide();
  };

  var autosaveTimer;
  var autosaveLabel = $("#autosave-label span");

  var resetSaveTimer = function(){
    clearTimeout(autosaveTimer);
    autosaveTimer = setTimeout(autosave, AUTOSAVE_INTERVAL);
  };

  var autosave = function(){
    var date = new Date();
    autosaveLabel.parent().css("visibility", "visible");
    autosaveLabel.text(date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds());
    autosaveLabel.text(date.toLocaleTimeString());
    autosaveTimer = null;
    createSubmission($('#autosave'), null);
  }

  var initializeEditors = function() {
    $('.editor').each(function(index, element) {
      var editor = ace.edit(element);

      if (qa_api) {
        editor.getSession().on("change", function (deltaObject) {
          qa_api.executeCommand('syncEditor', [active_file, deltaObject]);
        });
      }
      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 + ']');
      setActiveFile($(element).parent().data('filename'), file_id);

      document.insertLines(0, content.text().split(/\n/));
      editor.setReadOnly($(element).data('read-only') !== undefined);
      editor.setShowPrintMargin(false);
      editor.setTheme(THEME);
      editors.push(editor);
      var session = editor.getSession();
      session.setMode($(element).data('mode'));
      session.setTabSize($(element).data('indent-size'));
      session.setUseSoftTabs(true);
      session.setUseWrapMode(true);

      var file_id =  $(element).data('id');
      setAnnotations(editor, file_id);

      session.on('annotationRemoval', handleAnnotationRemoval);
      session.on('annotationChange', handleAnnotationChange);

      /*
       * Register event handlers
       */

      // editor itself
      editor.on("paste", handlePasteEvent);
      editor.on("copy", handleCopyEvent);
      editor.on("guttermousedown", handleSidebarClick);

      /* // alternative:
      editor.on("guttermousedown", function(e) {
        handleSidebarClick(e);
      });
      */

      //session
      session.on('annotationRemoval', handleAnnotationRemoval);
      session.on('annotationChange', handleAnnotationChange);

      // listener for autosave
      session.on("change", function (deltaObject) {
        resetSaveTimer();
      });
    });
  };

  var hasCommentsInRow = function (editor, row){
    return editor.getSession().getAnnotations().some(function(element) {
      return element.row === row;
    })
  };

  var getCommentsForRow = function (editor, row){
    return editor.getSession().getAnnotations().filter(function(element) {
      return element.row === row;
    })
  };

  var setAnnotations = function (editor, file_id){
      var session = editor.getSession();
      var url = "/comments";

      var jqrequest = $.ajax({
          dataType: 'json',
          method: 'GET',
          url: url,
          data: {
            file_id: file_id
          }
      });

      jqrequest.done(function(response){
          setAnnotationsCallback(response, session);
      });
      jqrequest.fail(ajaxError);
  };

  var setAnnotationsCallback = function (response, session) {
      var annotations = response;

      // add classname and the username in front of each comment
      $.each(annotations, function(index, comment){
          comment.className = "code-ocean_comment";
          comment.text = comment.username + ": " + comment.text;
      });

      session.setAnnotations(annotations);
  }

  var deleteComment = function (file_id, row, editor) {
      var jqxhr = $.ajax({
          type: 'DELETE',
          url: "/comments",
          data: {
            row: row,
            file_id: file_id          }
      });
      jqxhr.done(function (response) {
          setAnnotations(editor, file_id);
      });
      jqxhr.fail(ajaxError);
  }

  var createComment = function (file_id, row, editor, commenttext){
      var jqxhr = $.ajax({
        data: {
          comment: {
            file_id: file_id,
            row: row,
            column: 0,
            text: commenttext
          }
        },
        dataType: 'json',
        method: 'POST',
        url:  "/comments"
      });
      jqxhr.done(function(response){
          setAnnotations(editor, file_id);
      });
      jqxhr.fail(ajaxError);
  };

  var handleAnnotationRemoval = function(removedAnnotations) {
    removedAnnotations.forEach(function(annotation) {
      $.ajax({
        method: 'DELETE',
        url:  '/comment_by_id',
        data: {
          id: annotation.id,
        }
      })
    })
  };

  var handleAnnotationChange = function(changedAnnotations) {
    changedAnnotations.forEach(function(annotation) {
      $.ajax({
        method: 'PUT',
        url:  '/comments',
        data: {
          id: annotation.id,
          user_id: $('#editor').data('user-id'),
          comment: {
            row: annotation.row,
            text: annotation.text
          }
        }
      })
    })
  };

  // Code for clicks on gutter / sidepanel
  var handleSidebarClick = function(e) {
      var target  = e.domEvent.target;
      var editor =  e.editor;

      if (target.className.indexOf("ace_gutter-cell") == -1) return;
      if (!editor.isFocused()) return;
      if (e.clientX > 25 + target.getBoundingClientRect().left) return;

      var row = e.getDocumentPosition().row;
      e.stop();

      var commentModal = $('#comment-modal');

      if (hasCommentsInRow(editor, row)) {
        var rowComments = getCommentsForRow(editor, row);
        var comments = _.pluck(rowComments, 'text').join('\n');
        commentModal.find('#other-comments').text(comments);
      } else {
        commentModal.find('#other-comments').text('none');
      }

      commentModal.find('#addCommentButton').off('click');
      commentModal.find('#removeAllButton').off('click');

      commentModal.find('#addCommentButton').on('click', function(e){
        var commenttext = commentModal.find('textarea').val();
        // attention: use id of data attribute here, not file-id (file-id is the original file)
        var file_id = $(editor.container).data('id');

        if (commenttext !== "") {
          createComment(file_id, row, editor, commenttext);
          commentModal.modal('hide');
        }
      });

      commentModal.find('#removeAllButton').on('click', function(e){
        // attention: use id of data attribute here, not file-id (file-id is the original file)
        var file_id = $(editor.container).data('id');
        deleteComment(file_id,row, editor);
        commentModal.modal('hide');
      });

      commentModal.modal('show');
  };

  var initializeEventHandlers = function() {
    $(document).on('click', '#results a', showOutput);
    $(document).on('keypress', handleKeyPress);
    $('a[data-toggle="tab"]').on('show.bs.tab', storeTab);
    initializeFileTreeButtons();
    initializeWorkflowButtons();
    initializeWorkspaceButtons();
  };

  var initializeFileTree = function() {
    $('#files').jstree($('#files').data('entries'));
    $('#files').on('click', 'li.jstree-leaf', function() {
      active_file = {
        filename: $(this).text(),
        id: parseInt($(this).attr('id'))
      };
      var frame = $('[data-file-id="' + active_file.id + '"]').parent();
      showFrame(frame);
      toggleButtonStates();
    });
  };

  var initializeFileTreeButtons = function() {
    $('#create-file').on('click', showFileDialog);
    $('#destroy-file').on('click', confirmDestroy);
    $('#download').on('click', downloadCode);
    $('#request-for-comments').on('click', requestComments);
  };

  var initializeTooltips = function() {
    $('[data-tooltip]').tooltip();
  };

  var initializeWorkflowButtons = function() {
    $('#start').on('click', showWorkspaceTab);
    $('#submit').on('click', confirmSubmission);
  };

  var initializeWorkspaceButtons = function() {
    $('#assess').on('click', scoreCode); // todo
    $('#dropdown-render, #render').on('click', renderCode);
    $('#dropdown-run, #run').on('click', runCode);
    $('#dropdown-stop, #stop').on('click', stopCode); // todo
    $('#dropdown-test, #test').on('click', testCode); // todo
    $('#save').on('click', saveCode);
    $('#start-over').on('click', confirmReset);
  };

  var isActiveFileBinary = function() {
      return 'binary' in active_frame.data();
  };

  var isActiveFileExecutable = function() {
    return 'executable' in active_frame.data();
  };

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

  var isActiveFileRenderable = function() {
    return 'renderable' in active_frame.data();
  };

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

  var isActiveFileStoppable = function() {
    return isActiveFileRunnable() && running;
  };

  var isActiveFileSubmission = function() {
      return ['Submission'].includes(active_frame.data('contextType'));
  };

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

  var isBrowserSupported = function() {
    // todo event streams are no longer required with websockets
    return window.EventSource !== undefined;
  };

  var populatePanel = function(panel, result, index) {
    panel.removeClass('panel-default').addClass(getPanelClass(result));
    panel.find('.panel-title .filename').text(result.filename);
    panel.find('.panel-title .number').text(index + 1);
    panel.find('.row .col-sm-9').eq(0).find('.number').eq(0).text(result.passed);
    panel.find('.row .col-sm-9').eq(0).find('.number').eq(1).text(result.count);
    panel.find('.row .col-sm-9').eq(1).find('.number').eq(0).text((result.score * result.weight).toFixed(2));
    panel.find('.row .col-sm-9').eq(1).find('.number').eq(1).text(result.weight);
    panel.find('.row .col-sm-9').eq(2).text(result.message);
    panel.find('.row .col-sm-9').eq(3).find('a').attr('href', '#output-' + index);
  };

  var chunkBuffer = [{streamedResponse: true}];

  var printChunk = function(event) {
    var output = JSON.parse(event.data);
    if (output) {
        printOutput(output, true, 0);
        // send test response to QA
        // we are expecting an array of outputs:
        if (qa_api) {
            chunkBuffer.push(output);
        }
    } else {
      resetOutputTab();
    }
  };

  var resetOutputTab = function() {
      clearOutput();
      $('#hint').fadeOut();
      $('#flowrHint').fadeOut();
      showTab(1);
  }

  var printOutput = function(output, colorize, index) {
    var element = findOrCreateOutputElement(index);
    // disable streaming if desired
    //if (output.stdout && output.stdout.length >= 20 && output.stdout.substr(0,20) == "##DISABLESTREAMING##"){
    //    output_mode_is_streaming = false;
    //}
    if (!colorize) {
      var stream = _.sortBy([output.stderr || '', output.stdout || ''], function(stream) {
        return stream.length;
      })[1];
      element.append(stream);
    } else if (output.stderr) {
      element.addClass('text-warning').append(output.stderr);
    } else if (output.stdout) {
      //if (output_mode_is_streaming){
      element.addClass('text-success').append(output.stdout);
      //}else{
      //  element.addClass('text-success');
      //  element.data('content_buffer' , element.data('content_buffer') + output.stdout);
      //}
    //} else if (output.code && output.code == '200'){
    //  element.append( element.data('content_buffer'));
    } else {
      element.addClass('text-muted').text($('#output').data('message-no-output'));
    }
  };

  var printScoringResult = function(result, index) {
    $('#results').show();
    var panel = $('#dummies').children().first().clone();
    populatePanel(panel, result, index);
    $('#results ul').first().append(panel);
  };

  var printScoringResults = function(response) {
    $('#results ul').first().html('');
    $('.test-count .number').html(response.length);
    clearOutput();

    _.each(response, function(result, index) {
      printOutput(result, false, index);
      printScoringResult(result, index);
    });

    if (_.some(response, function(result) {
      return result.status === 'timeout';
    })) {
      showTimeoutMessage();
    }
    if (_.some(response, function(result) {
          return result.status === 'container_depleted';
      })) {
      showContainerDepletedMessage();
    }
    if (qa_api) {
      // send test response to QA
      qa_api.executeCommand('syncOutput', [response]);
    }
  };

    // Publishing events for other (JS) components to react to codeocean events
   var publishCodeOceanEvent = function (eventName, contextData) {

     var payload = {
       user: {
         resource_uuid: $('#editor').data('user-id')
       },
       verb: eventName,
       resource: {},
       timestamp: new Date().toISOString(),
       with_result: {},
       in_context: contextData
     };

     $.ajax("https://open.hpi.de/lanalytics/log", {
       type: 'POST',
       cache: false,
       dataType: 'JSON',
       data: payload,
       success: {},
       error: {}
      })

  };

  var renderCode = function(event) {
    event.preventDefault();
    if ($('#render').is(':visible')) {
      createSubmission(this, null, function(response) {
        var url = response.render_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
        var pop_up_window = window.open(url);
        if (pop_up_window) {
          pop_up_window.onerror = function(message) {
            clearOutput();
            printOutput({
              stderr: message
            }, true, 0);
            sendError(message, response.id);
            showTab(1);
          };
        }
      });
    }
  };

  var renderHint = function(object) {
    var hint = object.data || object.hint;
    if (hint) {
      $('#hint .panel-body').text(hint);
      $('#hint').fadeIn();
    }
  };

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

  var renderScore = function() {
    var score = $('#score').data('score');
    var maxium_score = $('#score').data('maximum-score');
    $('.score').html((score || '?') + ' / ' + maxium_score);
    renderProgressBar(score, maxium_score);
  };

  var resetCode = function() {
    showSpinner(this);
    ajax({
      method: 'GET',
      url: $('#start-over').data('url')
    }).success(function(response) {
      hideSpinner();
      _.each(editors, function(editor) {
        var file_id = $(editor.container).data('file-id');
        var file = _.find(response.files, function(file) {
          return file.id === file_id;
        });
        editor.setValue(file.content);
      });
    });
  };

  var runCode = function(event) {
    event.preventDefault();
    if ($('#run').is(':visible')) {
      runmode = WEBSOCKET;
      createSubmission(this, null, function(response) {
        $('#stop').data('url', response.stop_url);
        running = true;
        showSpinner($('#run'));
        toggleButtonStates();
        var url = response.run_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
        evaluateCode(url, true, printChunk);
      });
    }
  };

  var saveCode = function(event) {
    event.preventDefault();
    createSubmission(this, null, function() {
      $.flash.success({
        text: $('#save').data('message-success')
      });
    });
  };

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

  var scoreCode = function(event) {
    event.preventDefault();
    runmode = SERVER_SEND_EVENT;
    createSubmission(this, null, function(response) {
      showSpinner($('#assess'));
      var url = response.score_url;
      evaluateCode(url, false, handleScoringResponse);
    });
  };

  var showFileDialog = function(event) {
    event.preventDefault();
    createSubmission(this, null, function(response) {
      $('#code_ocean_file_context_id').val(response.id);
      $('#modal-file').modal('show');
    });
  };

  var showFirstFile = function() {
    var frame = $('.frame[data-role="main_file"]').isPresent() ? $('.frame[data-role="main_file"]') : $('.frame').first();
    var file_id = frame.find('.editor').data('file-id');
    setActiveFile(frame.data('filename'), file_id);
    $('#files').jstree().select_node(file_id);
    showFrame(frame);
    toggleButtonStates();
  };

  var showFrame = function(frame) {
    active_frame = frame;
    $('.frame').hide();
    frame.show();
  };

  var showOutput = function(event) {
    event.preventDefault();
    showTab(1);
    $('#output').scrollTo($(this).attr('href'));
  };

  var showRequestedTab = function() {
    if(REMEMBER_TAB){
      var regexp = /tab=(\d+)/;
      if (regexp.test(window.location.search)) {
        var index = regexp.exec(window.location.search)[1] - 1;
      } else {
        var index = localStorage.tab;
      }
    } else {
      // else start with first tab.
      var index = 0;
    }
    showTab(index);
  };

  var showSpinner = function(initiator) {
    $(initiator).find('i.fa').hide();
    $(initiator).find('i.fa-spin').show();
  };

  var showStatus = function(output) {
    if (output.status === 'timeout') {
      showTimeoutMessage();
    } else if (output.status === 'container_depleted') {
      showContainerDepletedMessage();
    } else if (output.stderr) {
      $.flash.danger({
        icon: ['fa', 'fa-bug'],
        text: $('#run').data('message-failure')
      });
    }
    /* do not show the success message any longer, puzzles and distracts users.
    else {
      $.flash.success({
        icon: ['fa', 'fa-check'],
        text: $('#run').data('message-success')
      });
    } */
  };

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

  var showTab = function(index) {
    $('a[data-toggle="tab"]').eq(index || 0).tab('show');
  };

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

  var showWebsocketError = function() {
    $.flash.danger({
      text: $('#flash').data('message-failure')
    });
  }

  var showWorkspaceTab = function(event) {
    event.preventDefault();
    showTab(0);
  };

  var stopCode = function(event) {
    event.preventDefault();
    if ($('#stop').is(':visible')) {
      if(runmode == WEBSOCKET){
        killWebsocketAndContainer();
      } else if (runmode == SERVER_SEND_EVENT) {
        stopCodeServerSendEvent(event);
      }
      runmode = NONE;
    }
  };

  var stopCodeServerSendEvent = function(event){
    var jqxhr = ajax({
      data: {
        container_id: $('#stop').data('container').id
      },
      url: $('#stop').data('url')
    });
    jqxhr.always(function() {
      hideSpinner();
      running = false;
      toggleButtonStates();
    });
    jqxhr.fail(ajaxError);
  };

  var killWebsocketAndContainer = function() {
    if (websocket.readyState != WebSocket.OPEN) {
      return;
    }
    websocket.send(JSON.stringify({cmd: 'exit'}));
    websocket.flush();
    websocket.close();
    hideSpinner();
    running = false;
    toggleButtonStates();
    hidePrompt();
    flashKillMessage();
  }

  var flashKillMessage = function() {
    $.flash.info({
      icon: ['fa', 'fa-clock-o'],
      text: "Your program was stopped." // todo get data attribute
    });
  }

  // todo set this from websocket command, required to e.g. stop container
  var storeContainerInformation = function(event) {
    var container_information = JSON.parse(event.data);
    $('#stop').data('container', container_information);

    if (_.size(container_information.port_bindings) > 0) {
      $.flash.info({
        icon: ['fa', 'fa-exchange'],
        text: _.map(container_information.port_bindings, function(key, value) {
          var url = window.location.protocol + '//' + window.location.hostname + ':' + key;
          return $('#run').data('message-network').replace('%{port}', value).replace(/%{address}/g, url);
        }).join('\n')
      });
    }
  };

  var storeTab = function(event) {
    localStorage.tab = $(event.target).parent().index();
  };

  var submitCode = function() {
    createSubmission($('#submit'), null, function(response) {
      if (response.redirect) {
        localStorage.removeItem('tab');
        window.location = response.redirect;
      }
    });
  };

  var testCode = function(event) {
    event.preventDefault();
    if ($('#test').is(':visible')) {
      createSubmission(this, null, function(response) {
        showSpinner($('#test'));
        var url = response.test_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
        evaluateCode(url, false, handleTestResponse);
      });
    }
  };

  var toggleButtonStates = function() {
    $('#destroy-file').prop('disabled', active_frame.data('role') !== 'user_defined_file');
    $('#dropdown-render').toggleClass('disabled', !isActiveFileRenderable());
    $('#dropdown-run').toggleClass('disabled', !isActiveFileRunnable() || running);
    $('#dropdown-stop').toggleClass('disabled', !isActiveFileStoppable());
    $('#dropdown-test').toggleClass('disabled', !isActiveFileTestable());
    $('#dummy').toggle(!fileActionsAvailable());
    $('#editor-buttons .dropdown-toggle').toggle(fileActionsAvailable());
    $('#render').toggle(isActiveFileRenderable());
    $('#run').toggle(isActiveFileRunnable() && !running);
    $('#stop').toggle(isActiveFileStoppable());
    $('#test').toggle(isActiveFileTestable());
    $('#request-for-comments').toggle(isActiveFileSubmission() && !isActiveFileBinary());
  };

  var initWebsocketConnection = function(url) {
      websocket = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port + url);
      websocket.onopen = function(evt) { resetOutputTab(); }; // todo show some kind of indicator for established connection
      websocket.onclose = function(evt) { /* expected at some point */ };
      websocket.onmessage = function(evt) { parseCanvasMessage(evt.data, true); };
      websocket.onerror = function(evt) { showWebsocketError(); };
      websocket.flush = function() { this.send('\n'); }
  };

  var initTurtle = function() {
      // todo guard clause if turtle is not required for the current exercise
      turtlescreen = new Turtle(websocket, turtlecanvas);
      if ($('#run').isPresent()) {
        $('#run').bind('click', hideCanvas);
      }
  };

  var initPrompt = function() {
      if ($('#run').isPresent()) {
          $('#run').bind('click', hidePrompt);
      }
      if ($('#prompt').isPresent()) {
          $('#prompt').on('keypress', handlePromptKeyPress);
          $('#prompt-submit').on('click', submitPromptInput);
      }
  }

  var executeWebsocketCommand = function(msg) {
      if ($.inArray(msg.cmd, commands) == -1) {
        console.log("Unknown command: " + msg.cmd);
        // skipping unregistered commands is required
        // as we may receive mirrored response due to internal behaviour
        return;
      }
      switch(msg.cmd) {
        case 'input':
            showPrompt();
            break;
        case 'write':
            printWebsocketOutput(msg);
            break;
        case 'turtle':
            showCanvas();
            handleTurtleCommand(msg);
            break;
        case 'turtlebatch':
            showCanvas();
            handleTurtlebatchCommand(msg);
            break;
        case 'exit':
            killWebsocketAndContainer();
            break;
        case 'status':
            showStatus(msg)
            break;
      }
  };

  var printWebsocketOutput = function(msg) {
      if (!msg.data) {
        return;
      }
      //msg.data = msg.data.replace(/(\r\n|\n|\r)/gm, "
"); msg.data = msg.data.replace(/(\r)/gm, "\n"); var stream = {}; stream[msg.stream] = msg.data; printOutput(stream, true, 0); }; var handleTurtleCommand = function(msg) { if (msg.action in turtlescreen) { result = turtlescreen[msg.action].apply(turtlescreen, msg.args); websocket.send(JSON.stringify({cmd: 'result', 'result': result})); } else { websocket.send(JSON.stringify({cmd: 'exception', exception: 'AttributeError', message: msg.action})); } websocket.flush(); }; var handleTurtlebatchCommand = function(msg) { for (i = 0; i < msg.batch.length; i++) { cmd = msg.batch[i]; turtlescreen[cmd[0]].apply(turtlescreen, cmd[1]); } }; var handlePromptKeyPress = function(evt) { if (evt.which === ENTER_KEY_CODE) { submitPromptInput(); } } var submitPromptInput = function() { var input = $('#prompt-input'); var message = input.val(); websocket.send(JSON.stringify({cmd: 'result', 'data': message})); websocket.flush(); input.val(''); hidePrompt(); } var parseCanvasMessage = function(message, recursive) { var msg; message = message.replace(/^\s+|\s+$/g, ""); try { // todo validate json instead of catching msg = JSON.parse(message); } catch (e) { if (!recursive) { return false; } // why does docker sometimes send multiple commands at once? message = message.replace(/^\s+|\s+$/g, ""); messages = message.split("\n"); for (var i = 0; i < messages.length; i++) { if (!messages[i]) { continue; } parseCanvasMessage(messages[i], false); } return; } executeWebsocketCommand(msg); }; var showPrompt = function() { if (prompt.isPresent() && prompt.hasClass('hidden')) { prompt.removeClass('hidden'); } prompt.focus(); } var hidePrompt = function() { if (prompt.isPresent() && !prompt.hasClass('hidden')) { prompt.addClass('hidden'); } } var showCanvas = function() { if ($('#turtlediv').isPresent() && turtlecanvas.hasClass('hidden')) { // initialize two-column layout $('#output-col1').addClass('col-lg-7 col-md-7 two-column'); turtlecanvas.removeClass('hidden'); } }; var hideCanvas = function() { if ($('#turtlediv').isPresent() && !(turtlecanvas.hasClass('hidden'))) { output = $('#output-col1'); if (output.hasClass('two-column')) { output.removeClass('col-lg-7 col-md-7 two-column'); } turtlecanvas.addClass('hidden'); } }; var requestComments = function(e) { var user_id = $('#editor').data('user-id') var exercise_id = $('#editor').data('exercise-id') var file_id = $('.editor').data('id') $.ajax({ method: 'POST', url: '/request_for_comments', data: { request_for_comment: { exercise_id: exercise_id, file_id: file_id, "requested_at(1i)": 2015, // these are the timestamp values that the request handler demands "requested_at(2i)":3, // they could be random here, because the timestamp is updated on serverside anyway "requested_at(3i)":27, "requested_at(4i)":17, "requested_at(5i)":06 } } }).done(function() { hideSpinner() $.flash.success({ text: 'Request for comments sent!' }) }) showSpinner($('#request-for-comments')) // hide button until next submission is created $('#request-for-comments').toggle(false); } var initializeCodePilot = function() { if ($('#questions-column').isPresent() && (typeof QaApi != 'undefined') && QaApi.isBrowserSupported()) { $('#editor-column').addClass('col-md-8').removeClass('col-md-10'); $('#questions-column').addClass('col-md-3'); var node = document.getElementById('questions-holder'); var url = $('#questions-holder').data('url'); qa_api = new QaApi(node, url); } } // save on quit $(window).on("beforeunload", function() { if(autosaveTimer){ autosave(); } }); if ($('#editor').isPresent()) { if (isBrowserSupported()) { initializeCodePilot(); $('.score, #development-environment').show(); configureEditors(); initializeEditors(); initializeEventHandlers(); initializeFileTree(); initializeTooltips(); initPrompt(); renderScore(); showFirstFile(); showRequestedTab(); } else { $('#alert').show(); } } });