<commit_before><link href="https://fonts.googleapis.com/css?family=Nunito:700" rel="stylesheet">

<link href="./gs-element-blockly.html" rel="import"/>
<link href="./gobstones-code-runner.html" rel="import"/>
<script src="./hammer.min.js"></script>

<script>
  Blockly.FieldImage.prototype.__setValue__ = Blockly.FieldImage.prototype.setValue;
  Blockly.FieldImage.prototype.setValue = function(a) {
    if (a && a.startsWith("undefined")) {
      console.debug(`Ignoring unavailable image ${a}`);
    } else if (a) {
      const absoluteUrl = a
        .replace("../media/", "../local-media/")
        .replace("../local-media/", "<%= @assets_url %>/local-media/");
      this.__setValue__(absoluteUrl);
    } else {
      this.__setValue__(a);
    }
  }

  Blockly.FieldImage.prototype.__setTooltip__ = Blockly.FieldImage.prototype.setTooltip;
  Blockly.FieldImage.prototype.setTooltip = function(a) {
    try {
      this.__setTooltip__(a);
    } catch (_e) {
      console.debug('Could not set tooltip');
    }
  }

  function postpone(action) {
    return setTimeout(action, 50);
  }
</script>

<dom-module id="mu-gobstones-custom-editor">

  <template>
    <gs-element-blockly id="blocklyElement"
                        read-only="{{readOnly}}"
                        media="{{mediaUrl}}"
                        local-media="{{localMediaUrl}}"
    ></gs-element-blockly>
    <gs-element-blockly id="blocklyTmp"
                        style="display: none"
                        media="{{mediaUrl}}"
                        localMediaUrl="{{localMediaUrl}}"
    ></gs-element-blockly>
  </template>

  <script>
    const compilationModes = {
      gameFramework: {
        compile: ({ main, teacher, ...args }) => {
          const extra = `<%= @game_framework_extra %>`;
          const program = `<%= @game_framework_program %>`;

          const append = (code, anotherCode) => `${code}\n\n${anotherCode}`

          return {
            main: append(main, program),
            teacher: append(teacher, extra),
            ...args
          };
        },

        scrollToMainBlock: (blockly) => {
          const mainBlock = blockly.getBlocksOfType('procedures_defnoreturnnoparams')[0];
          if (mainBlock) {
            blockly.scrollToBlock(mainBlock.id);
          }
        },

        defaultCode: `<%= @game_framework_default %>`
      },
      classic: {
        compile: (code) => code,

        scrollToMainBlock: (blockly) => {
          blockly.scrollToBlock();
        },

        defaultCode: ''
      }
    };

    Polymer({
      is: 'mu-gobstones-custom-editor',
      properties: {
        mediaUrl: {
          type: String,
          value: "<%= @assets_url %>/media/"
        },
        localMediaUrl: {
          type: String,
          value: "<%= @assets_url %>/local-media/"
        },
        readOnly: {
          type: Boolean,
          value: false
        },
        teacherMode: {
          type: Boolean,
          default: false
        },
        defaultIcons: {
          type: Object,
          value: {
            procedureDeclarations: { /** for future use */ },
            functionDeclarations: { /** for future use */ }
          }
        }
      },

      // ==== Polymer events

      attached() {
        this.$exerciseContainer = $('.mu-kids-exercise');

        /*
        https://polymer-library.polymer-project.org/1.0/docs/devguide/registering-elements#initialization-order

        There are no guarantees with regard to initialization timing between sibling elements.
        This means that siblings may become ready in any order.
        For accessing sibling elements when an element initializes, you can call async from inside the attached callback.
        */
        this.async(() => {
          this._configureBlocklyAppearance();
          this._configureBlocklyBehavior();
        });
      },

      // ==== Public methods (accesible from other components)

      reset() {
        const blockly = this._getBlockly();
        blockly.workspaceXml = blockly.initialXml;
        this._scrollToMainBlock();
      },

      /**
       * Configures a custom toolbox using a toolbox url
       * This method does nothing if editor is read-only, but
       * assumes a gs-toolbox element is present - otherwise it would
       * have no sense to have a toolboxUrl.
       */
      configureCustomToolboxFromUrl(toolboxUrl) {
        if (this.readOnly) return;

        console.debug('Configuring custom toolbox');

        $.get(toolboxUrl, (toolboxXml) => {
          this._initializeCustomToolboxBlocklyWorkspace(toolboxXml);
        });
      },

      /**
       * Wether a custom toolbox is required. Read only editors and gs-toolbox-less context
       * don't require it
       */
      hasCustomToolbox() {
        return !this.readOnly && $('gs-toolbox').length;
      },

      compile(code) {
        return this._compilationMode().compile(code);
      },

      hasInteractiveProgram() {
        return this._getBlockly().initialXml.indexOf("block type=\"InteractiveProgram\"") !== -1;
      },

      toggleInteractiveMode() {
        this.$exerciseContainer.toggleClass('play-mode');
        this._triggerResize();
      },

      getTeacherCode() {
        const extraCode = this._getExtraCode();
        if (!extraCode) return;
        this.$.blocklyTmp.workspaceXml = extraCode;
        return this.$.blocklyTmp.generateCode();
      },

      getStudentCode: function () {
        return this
          ._getBlockly()
          .generateCode({withRegions: true, clearErrors: false});
      },

      getStudentXml: function () {
        return this._getStudentSolution() || "";
      },

      getTestCode() {
        return this._getEditorTest().value;
      },

      removeBlockErrors() {
        return this._getBlockly().workspace.removeBlockErrors();
      },

      showBlockError(region, error) {
        this._getBlockly().scrollToBlock(region);
        this._getBlockly().showBlockError(region, error.message);
      },

      highlightBlock(region) {
        this._getBlockly().highlightBlock(region);
      },

      // ==== Private methods (only for internal usage)

      _configureBlocklyAppearance() {
        this._getBlockly().showCategories = !this._isKindergarten();
        this._setBlocklyDisplayMode();
        this._setBlocklySounds();
        this._setBlocklyColors();
        this._relocateTrashOnResize();

        // Removing "/" from the block id character set to avoid syntax errors
        Blockly.utils.genUid.soup_ = Blockly.utils.genUid.soup_.replace("/", "");
      },

      _configureBlocklyBehavior() {
        this._setTeacherActions();
        this._setGameActions();
        this._initializeNonCustomToolboxBlocklyWorkspace();
        this._registerLayoutChangedEvent();
      },

      _relocateTrashOnResize() {
        if (this.readOnly) {
          console.debug("No trash relocate on resize required");
          return;
        }

        $('.mu-kids-context, .mu-kids-results').on('hidden.bs.modal shown.bs.modal', () => {
          this._relocateTrash();
        });

        $(window).resize(() => this._relocateTrash());
      },


      /**
       * Initializes a workspace using a custom toolbox.
       *
       * Blockly's workspace is destroyed when toolbox changes, so initialization
       * is performed here

       * @see _initializeBlocklyWorkspace
       */
      _initializeCustomToolboxBlocklyWorkspace(toolboxXml) {
        this._getBlockly().toolbox = { defaultToolbox: toolboxXml };
        this._initializeBlocklyWorkspace();
      },

      /**
       * Initializes a workspace using a non-custom toolbox.
       *
       * Blockly's workspace is destroyed when toolbox changes,
       * so this method will initialize it only if a there is a custom toolbox
       * that will be called later.
       *
       * @see _initializeBlocklyWorkspace
       */
      _initializeNonCustomToolboxBlocklyWorkspace() {
        if (!this.hasCustomToolbox()) {
          this._initializeBlocklyWorkspace();
        }
      },

      /**
       * Performs actual workspace (re)creation,
       * which sets initial xml, user solution, trash position
       * and appropriate callbacks.
       *
       * This method is the actual culmination of the blockly initialization
       * process
       */
      _initializeBlocklyWorkspace() {
        console.debug('Initializing Blockly Workspace');

        this._setInitialXml();
        this._initializeWorkspace();
        this._relocateTrash();
        this._subscribeToWorkspace(() => this._updateSolution());

        if (this.hasInteractiveProgram()) {
          this._setInteractiveLayout();
        }

        mumuki.assetsLoadedFor('editor');
      },

      _relocateTrash() {
        if (this.readOnly) {
          console.debug("No trash relocate required");
          return;
        }

        const $blocklyTrash = $('.blocklyTrash');
        const width = $('#blocklyDiv').width() - 68;
        const height = $('#blocklyDiv').height() - 210;
        $blocklyTrash.css("transform", `translate(${width}px, ${height}px)`);
        $blocklyTrash.css("display", "unset");
      },

      _updateSolution() {
        console.debug("Updating solution");
        const blockly = this._getBlockly();
        let changed;
        if (this.teacherMode) {
          changed = this._setExtraCode(blockly.workspaceXml);
        } else {
          changed = this._setStudentSolution(blockly.workspaceXml);
        }

        if (!changed) {
          console.debug("No solution update required")
          return;
        }

        // TODO: this hack enables Angular two-way binding for Bibliotheca. Should be replaced with Mumuki events system
        if (typeof angular !== 'undefined') {
          angular.element(this._getStudentEditor()).triggerHandler("change");
          angular.element(this._getEditorExtra()).triggerHandler("change");
        }

        this._stopExecution();
      },

      _setBlocklySounds() {
        Blockly.WorkspaceAudio.prototype.preload = () => {
          for (const soundName in this.SOUNDS_) {
            const sound = this.SOUNDS_[soundName];
            sound.volume = 0.01;
            const playPromise = sound.play();
            playPromise && playPromise.then(sound.pause.bind(sound));
            if (goog.userAgent.IPAD || goog.userAgent.IPHONE) break;
          }
        };
      },

      _setBlocklyColors() {
        Blockly.HSV_SATURATION = 0.64;
        Blockly.HSV_VALUE = 1;

        Blockly.MUMUKI_COLORS = {
          pink: "#FF5C82",
          blue: "#5CBEFF",
          yellow: "#FFC95C",
          green: "#5CED71"
        };

        // reserved
        Blockly.CUSTOM_COLORS.program = Blockly.MUMUKI_COLORS.pink;
        Blockly.CUSTOM_COLORS.interactiveProgram = Blockly.MUMUKI_COLORS.pink;
        Blockly.CUSTOM_COLORS.interactiveBinding = Blockly.MUMUKI_COLORS.pink;
        Blockly.CUSTOM_COLORS.procedure = Blockly.MUMUKI_COLORS.pink;
        Blockly.CUSTOM_COLORS.function = Blockly.MUMUKI_COLORS.pink;
        Blockly.CUSTOM_COLORS.complete = Blockly.MUMUKI_COLORS.pink;

        // control structures
        Blockly.CUSTOM_COLORS.controlStructure = this._isKindergarten() ? Blockly.MUMUKI_COLORS.green : Blockly.MUMUKI_COLORS.yellow;

        // commands
        Blockly.CUSTOM_COLORS.primitiveCommand = Blockly.MUMUKI_COLORS.yellow;
        Blockly.CUSTOM_COLORS.primitiveProcedure = Blockly.MUMUKI_COLORS.yellow;
        Blockly.CUSTOM_COLORS.procedure_call = Blockly.MUMUKI_COLORS.yellow;
        Blockly.CUSTOM_COLORS.assignation = Blockly.MUMUKI_COLORS.yellow;

        // expressions
        Blockly.Msg.MATH_HUE = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.literalExpression = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.primitiveExpression = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.operator = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.variable = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.parameter = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.primitiveFunction = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.function_call = Blockly.MUMUKI_COLORS.blue;
      },

      _setBlocklyDisplayMode() {
        // The display mode configuration could be monkey-patched here, like this:
        Blockly.displayModes.iconic.iconSize = 64;
        Blockly.displayModes.iconic.procedureDefIcon = `${this.localMediaUrl}main-procedure.png`;

        Blockly.displayMode = this._isKindergarten() ? 'iconic' : 'text';
      },

      _triggerResize() {
        const event = document.createEvent('HTMLEvents');
        event.initEvent('resize', true, false);
        document.dispatchEvent(event);
      },

      _setInteractiveLayout() {
        this.$exerciseContainer.addClass('mu-kids-interactive');
        $('.mu-final-state').html('<gs-keyboard/>');
        $('.mu-editor').append($('<kids-interactive-submit-button/>'));
        // In order to avoid triggering click on enter or space
        $('#gbsPlayButton')[0].addEventListener('focus', function () {
          this.blur();
        })
      },

      _setTeacherActions() {
        const teacherCode = this.getTeacherCode();
        if (!teacherCode) {
          return;
        }

        console.debug("Set teacher actions");
        const actions = new Parser().getActionsFromSource(teacherCode);

        this._setPrimitiveProcedures(this._withDefaultIcons(actions, 'procedureDeclarations'));
        this._setPrimitiveFunctions(this._withDefaultIcons(actions, 'functionDeclarations'));
      },

      _setGameActions() {
        if (!this._isGame()) {
          return;
        }

        console.debug("Set game actions");
        const blockly = this._getBlockly();

        this._setPrimitiveProcedures((blockly.primitiveProcedures || []).concat([
          this._gamePrimitive('ShiftUp'),
          this._gamePrimitive('ShiftDown'),
          this._gamePrimitive('ShiftLeft'),
          this._gamePrimitive('ShiftRight')
        ]));
      },

      _setPrimitiveProcedures(procedures) {
        this._setBlocklyActions("primitiveProcedures", procedures);
      },

      _setPrimitiveFunctions(functions) {
        this._setBlocklyActions("primitiveFunctions", functions);
      },

      _setBlocklyActions(key, actions) {
        try {
          this._getBlockly()[key] = actions
        } catch (e) {
          if (this._getBlockly()[key] !== actions) {
            throw e;
          }
        }
      },

      _gamePrimitive(name) {
        return { alias: 'procedureDeclaration', name, attributes: {block_icon: `<%= @assets_url %>/media/${name}.png`} };
      },

      _getBlockly() {
        return this.$.blocklyElement;
      },

      _getStudentSolution() {
        return this._getStudentEditor().value;
      },

      /**
       * Sets the student solution xml, which corresponds
       * to user content in laboratory, and default content in bibliotheca
       */
      _setStudentSolution(code) {
        if (this._getStudentEditor().value !== code) {
          console.debug(`Setting student ${code}`)
          this._getStudentEditor().value = code;
          return true;
        }
        return false;
      },

      _getDefaultCode() {
        return this._getEditorDefaultValue().value;
      },

      _getExtraCode() {
        return this._getEditorExtra().value;
      },

      _setExtraCode(code) {
        if (this._getEditorExtra().value !== code) {
          console.debug(`Setting extra ${code}`)
          this._getEditorExtra().value = code;
          return true;
        }
        return false;
      },

      _getEditorExtra: function() {
        return $("#mu-custom-editor-extra")[0] || {};
      },

      _getEditorTest: function() {
        return $("#mu-custom-editor-test")[0];
      },

      _getStudentEditor: function() {
        return $("#mu-custom-editor-value")[0] || {};
      },

      _getEditorDefaultValue: function() {
        return $("#mu-custom-editor-default-value")[0] || {};
      },

      _stopExecution() {
        const submit = $("kids-submit-button")[0];
        if (submit && submit.$.runner.isRunning) {
          submit.$.runner.stop();
        }
      },

      _registerLayoutChangedEvent() {
        if (!this.teacherMode) {
          return;
        }

        mumuki.events.on('layoutChanged', () => {
          console.debug('layout changed');
          this._setStudentSolution(this._compilationMode().defaultCode);
        });
      },

      _scrollToMainBlock() {
        this._compilationMode().scrollToMainBlock(this._getBlockly());
      },

      _withDefaultIcons(actions, type) {
        const addIcon = (declaration, block_icon) =>
          ({ ...declaration, attributes: { ...declaration.attributes, block_icon } });

        return actions[type].map((declaration) => {
          const defaultIcon = this.defaultIcons[type][declaration.name];
          if (defaultIcon) {
            return addIcon(declaration, defaultIcon);
          } else {
            return declaration;
          }
        });
      },

      _isKindergarten() {
        return mumuki.exercise.layout === 'input_kindergarten';
      },

      _isGame() {
        if (mumuki.exercise.settings) {
          return mumuki.exercise.settings.game_framework;
        } else {
          // for backwards compatibility
          return this._isKindergarten();
        }
      },

      _compilationMode() {
        return this._isGame() ? compilationModes.gameFramework : compilationModes.classic;
      },

      _setInitialXml: function () {
        const blockly = this._getBlockly();
        const defaultCode = this._getDefaultCode();
        if (defaultCode) {
          blockly.initialXml = defaultCode;
        } else {
          blockly.initialXml = blockly.workspaceXml;
        }
      },

      _initializeWorkspace() {
        const blockly = this._getBlockly();
        const value = this._getStudentSolution();

        blockly.workspaceXml = value || (
          this.teacherMode
            ? "<xml></xml>"
            : blockly.initialXml
        );

        this._scrollToMainBlock();
      },

      _subscribeToWorkspace: function(action) {
        this._getBlockly().workspace.addChangeListener(action);
      }
    });
  </script>
</dom-module>

<dom-module id="waiting-spinner">
  <style>
    .spinner {
      width: 60px;
      height: 60px;
    }

    .double-bounce1, .double-bounce2 {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background-color: #ff4081;
      opacity: 0.6;
      position: absolute;
      top: 0;
      left: 0;

      -webkit-animation: sk-bounce 2.0s infinite ease-in-out;
      animation: sk-bounce 2.0s infinite ease-in-out;
    }

    .double-bounce2 {
      -webkit-animation-delay: -1.0s;
      animation-delay: -1.0s;
    }

    @-webkit-keyframes sk-bounce {
      0%, 100% {
        -webkit-transform: scale(0.0)
      }
      50% {
        -webkit-transform: scale(1.0)
      }
    }

    @keyframes sk-bounce {
      0%, 100% {
        transform: scale(0.0);
        -webkit-transform: scale(0.0);
      }
      50% {
        transform: scale(1.0);
        -webkit-transform: scale(1.0);
      }
    }
  </style>

  <template>
    <div class="spinner">
      <div class="double-bounce1"></div>
      <div class="double-bounce2"></div>
    </div>
  </template>

  <script>
    Polymer({
      is: "waiting-spinner"
    });
  </script>
</dom-module>

<dom-module id="kids-interactive-submit-button">
  <style>
    .mu-kids-interactive-submit-button {
      position: absolute;
      right: 80px;
      width: 56px;
      height: 56px;
      bottom: 15px;
      z-index: 70;
    }

    .mu-kids-interactive-submit-button paper-fab {
      background-color: #0B456D;
    }

  </style>

  <template>
    <div class="mu-kids-interactive-submit-button">
      <paper-fab icon="av:skip-next"
                 on-click="_onButtonClick"
      ></paper-fab>
    </div>
  </template>

  <script>
    Polymer({
      is: 'kids-interactive-submit-button',

      ready: function () {
        this.submitButton = $("kids-submit-button")[0];
        this.codeRunner = this.submitButton.$.runner;
      },

      _onButtonClick: function () {
        const currentPromise = this.submitButton.serverPromise;
        if(!currentPromise) return;
        currentPromise.always(() => {
          const currentResponse = this.submitButton.serverResponse;
          this.codeRunner.stop();
          mumuki.kids.showResult(currentResponse);
          this.codeRunner.isDirty = true;
        })
      },
    });
  </script>
</dom-module>

<dom-module id="kids-reset-button">
  <template>
    <div>
      <paper-fab id="gbsResetButton"
                 icon="av:replay"
                 on-click="_onButtonClick"
      ></paper-fab>
    </div>
  </template>

  <script>
    Polymer({
      is: 'kids-reset-button',

      _onButtonClick: function () {
        this._getEditor().reset();
      },

      _getEditor: function () {
        return $("mu-gobstones-custom-editor")[0];
      }
    });
  </script>
</dom-module>

<dom-module id="kids-submit-button">
  <style>
    .hidden {
      visibility: hidden;
    }

    .visible {
      visibility: visible;
    }

    .spinner {
      position: absolute;
      top: 0;
      left: 0;
      transform: translate(-2px, -2px);
    }
  </style>

  <template>
    <div style="position: relative">
      <gobstones-code-runner id="runner" class$="{{_getSubmitCss(isWaiting)}}"></gobstones-code-runner>
      <waiting-spinner class$="spinner {{_getSpinnerCss(isWaiting)}}"></waiting-spinner>
    </div>
  </template>

  <script>
    Polymer({
      is: "kids-submit-button",
      listeners: {
        "gbs-run-request": "_onRunRequest",
        "gbs-stop": "_onStop",
        "gbs-reset-state": "_onResetState"
      },
      properties: {
        serverPromise: Object,
        serverResponse: Object,
        isWaiting: {
          type: Boolean,
          value: false
        }
      },

      ready: function () {
        const resetStatusAfterAborted = () => {
          $('.mu-kids-results-aborted').on('hidden.bs.modal', () => {
            this.$.runner.isDirty = false;
            this._onResetState();
          })
        };
        const _getScenarioSelector = function (scenario) {
          return `.mu-${scenario}-state .mu-kids-state-image > *`;
        };

        const _createMultipleScenarios = () => {
          const scenarios = ['initial', 'final'];
          const scenarioSelectors = scenarios.map(it => _getScenarioSelector(it));
          this.multipleScenarios = new mumuki.MultipleScenarios(scenarioSelectors);
        };

        const setInitialState = () => {
          const initialBoards = this._getInitialBoards();
          if (!initialBoards[0].size) return postpone(setInitialState);

          this.initialState = initialBoards.toArray().map((board) => this._boardToState(board));

          resetStatusAfterAborted();
        };

        _createMultipleScenarios();
        setInitialState();
        this.$editor = $("mu-gobstones-custom-editor")[0];
      },

      _boardToState : function ({size, table, header, boom}) {
        return {size, table, header, boom}
      },

      _onRunRequest: function (event) {
        const {detail: controller} = event;

        const editor = this.$editor;
        const xml = editor.getStudentXml();
        const code = editor.getStudentCode();
        const teacherCode = editor.getTeacherCode() || "";
        const finalBoard = this._getTargetBoard();
        const solution = {content: xml};
        const executionSpeed = 2;

        this._cleanState();
        this._cleanErrors(finalBoard);

        const targetStateIndex = this.multipleScenarios.currentScenarioIndex;
        this.targetState = this.initialState[targetStateIndex];

        const promise = new mumuki.bridge.Laboratory()
          .runTests(solution)
          .then((results) => {
            if (promise !== this.serverPromise) return;

            this._onRemoteExecutionStop(results);
          })
          .catch(() => {
            mumuki.kids.showResult({status: 'aborted'})
          });
        this.serverPromise = promise;

        editor.toggleInteractiveMode();

        controller.start({
          initialState: this.targetState,
          code: editor.compile({
            main: code,
            library: "",
            teacher: teacherCode
          })
        }, {
          onResult: (state, fullState) => {
            const region = this._getLastRegion(fullState);
            if (region) this._highlight(region);

            this._updateBoard(state, finalBoard);
          },
          onCompilationError: (error) => {
            const {region} = error.on;
            if (region) this._showError(region, error);
          }
        }, executionSpeed);
      },

      _onStop: function (event) {
        const reason = event.detail;

        const editor = this.$editor;

        if (reason === "end") {
          this._onExecutionEnd(editor);
        }
        else if (reason === "cancel" && editor.interactiveMode) {
          this._onInteractiveStop(editor);
        }
        else this.serverPromise = undefined;
      },

      _onExecutionEnd: function (editor) {
        if(editor.interactiveMode) {
          editor.toggleInteractiveMode();
        } else {
          this._onLocalExecutionStop();
          this._toggleInitialState();
        }
        this.$.runner.isDirty = true;
      },

      _onInteractiveStop: function (editor) {
        this._removeBlockErrors();
        this._cleanState();
        this._resetBoards();
        editor.toggleInteractiveMode();
      },

      _onLocalExecutionStop: function () {
        if (!this.serverResponse) this.isWaiting = true;
        else this._onExecutionStop(this.serverResponse);
      },

      _onRemoteExecutionStop: function (serverResponse) {
        this.serverResponse = serverResponse;
        if (!this.$.runner.isRunning && !this.$editor.interactiveMode) {
          this._onExecutionStop(serverResponse);
        }
      },

      _onExecutionStop: function (data) {
        this._cleanState();
        this._setResultsBoards(data.test_results);
        mumuki.kids.showResult(data);
      },

      _setResultsBoards: function (testResults) {
        if (!testResults) return;
        const testResultsBoards = this._testResultsBoards(testResults);
        this._updateBoards(testResultsBoards);
        const testResultsStatuses = this._testResultsStatuses(testResults);
        this.multipleScenarios.updateIndicators(testResultsStatuses);
      },

      _testResultsBoards: function (testResults) {
        return testResults.map(it => $(it.result).find('.actual_board > *, .final_board > *')[0]);
      },

      _testResultsStatuses: function (testResults) {
        return testResults.map(it => it.status);
      },

      _updateBoards: function (resultsBoards) {
        const initialBoards = this.multipleScenarios.scenarios[0];
        resultsBoards.forEach((resultScenario, index) => {
          if (this.multipleScenarios.currentScenarioIndex === index) return;
          const currentBoard = initialBoards[index];
          this._updateBoardFromResult(currentBoard.firstChild, resultScenario);
        });
      },

      _updateBoardFromResult: function (originalBoard, resultBoard) {
        // In Firefox the gbb is present in the textContent attribute while in Chrome it's in the gbb attribute.
        const resultGbb = resultBoard.textContent || resultBoard.gbb;
        if (resultGbb) {
          const boardAttrs = gbbReader.fromString(resultGbb);
          originalBoard.update(boardAttrs.table, boardAttrs.head);
        }
        else {
          resultBoard = resultBoard.cloneNode(); // To force initialization
          originalBoard.boom = resultBoard.boom;
        }
      },

      _updateBoard: function (state, finalBoard) {
        const {error, table, head} = state;

        finalBoard.boom = error != null;

        if (error) {
          const region = this._getLastRegion(error.on);
          if (region) this._showError(region, error);
        } else {
          finalBoard.update(table, head);
        }

        this._removeBorder(finalBoard);
      },

      _removeBorder(board) {
        board._setBorderOff();
        board.updateStyles();
      },

      _highlight: function (region) {
        this.$editor.highlightBlock(region);
      },

      _showError: function (region, error) {
        const expectsTimeout = /expect_endless_while *: *true/.test(
          this.$editor.getTestCode()
        );

        const isExpectedTimeout = (
          error.reason && error.reason.code === "timeout" &&
          expectsTimeout
        );

        if (isExpectedTimeout) return;

        this.$editor.showBlockError(region, error);
      },

      _cleanState: function () {
        this.serverPromise = undefined;
        this.serverResponse = undefined;
        this.isWaiting = false;
      },

      _cleanErrors: function (finalBoard) {
        this._removeBlockErrors();
        finalBoard.boom = false;
      },

      _removeBlockErrors() {
        this.$editor.removeBlockErrors();
      },

      _getLastRegion: function (context = {}) {
        const {regionStack} = context;
        return regionStack && regionStack.filter(it => it).slice(-1)[0];
      },

      _getInitialBoards: function () {
        return $(".mu-initial-state gs-board");
      },

      _getTargetBoard: function () {
        return this._getInitialBoards().parent('.active').children()[0];
      },

      _getSubmitCss: function (isWaiting) {
        return isWaiting ? "hidden" : "visible";
      },

      _getSpinnerCss: function (isWaiting) {
        return isWaiting ? "visible" : "hidden";
      },

      _resetBoards: function () {
        const boards = this._getInitialBoards();
        this.initialState.forEach(({table, header, boom}, index) => {
          const board = boards[index];
          board.update(table, header);
          board.boom = boom;
        });
      },

      _onResetState: function () {
        this._removeBlockErrors();
        this._resetBoards();
        this._toggleInitialState();
        this.multipleScenarios.resetIndicators();
        mumuki.kids.restart();
      },

      _toggleInitialState: function () {
        if(this.$editor.interactiveMode) return;
        $("#mu-initial-state-text").toggle();
        $("#mu-actual-state-text").toggle();
      }
    });
  </script>
</dom-module>

<dom-module id="gs-toolbox">
  <script>
    Polymer({
      is: 'gs-toolbox',
      properties: {
        toolboxUrl: Object,
        observer: '_setEditorToolbox'
      },

      attached: function () {
        this.async(() => {
          this._setEditorToolbox();
        });
      },

      _setEditorToolbox: function () {
        const $editor = $("mu-gobstones-custom-editor")[0];

        // there is no editor to configure
        if (!$editor) return;

        $editor.configureCustomToolboxFromUrl(this.toolboxUrl);
      }
    });
  </script>
</dom-module>

<dom-module id="gs-keyboard-arrow-keys">
  <template>
    <div class="gs-keyboard-arrow-keys-container">
      <svg class="keys-svg" viewBox="0 0 100 100" height="100%">
        <path d="M50,0 60,10 60,26 40,26 40,10Z" data-direction="DIRECTION_UP" on-tap="dispatchArrowPressed"></path>
        <path d="M50,100 60,90 60,74 40,74 40,90Z" data-direction="DIRECTION_DOWN" on-tap="dispatchArrowPressed"></path>
        <path d="M0,50 10,60 26,60 26,40 10,40Z" data-direction="DIRECTION_LEFT" on-tap="dispatchArrowPressed"></path>
        <path d="M100,50 90,60 74,60 74,40 90,40Z" data-direction="DIRECTION_RIGHT" on-tap="dispatchArrowPressed"></path>
      </svg>
    </div>
  </template>

  <style>
    .gs-keyboard-arrow-keys path {
      fill: #0B456D;
      cursor: pointer;
    }

    .gs-keyboard-arrow-keys-container {
      height: 100%;
      width: 100%;
      position: relative;
    }

    .keys-svg {
      display: block;
      margin: 0 auto;
      padding-left: 10px;
      overflow: visible;
    }

    .keys-svg path {
      filter: drop-shadow(2px 2px 2px rgba(11, 70, 93, .4));
      -webkit-filter: drop-shadow(2px 2px 2px rgba(11, 70, 93, .4));
    }

    .keys-svg path.pressed {
      filter: drop-shadow(1px 1px 1px rgba(11, 70, 93, .4));
      -webkit-filter: drop-shadow(1px 1px 1px rgba(11, 70, 93, .4));
      transform: translate(1px, 1px);
    }
  </style>

  <script>
    Polymer({
      is: 'gs-keyboard-arrow-keys',
      properties: {
        dispatchKeydown: {
          type: Object
        }
      },
      ready: function () {
      },
      clickTransition: function (direction) {
        const arrow = $(this).find('path[data-direction=' + direction +']');
        arrow.toggleClass('pressed');
        setTimeout(() => arrow.toggleClass('pressed'), 300);
      },
      dispatchArrowPressed: function (event) {
        const direction = event.target.dataset.direction;
        this.dispatchKeydown(Hammer[direction]);
        this.clickTransition(direction);
      }
    })
  </script>
</dom-module>

<dom-module id="gs-keyboard-key">
  <template>
    <button class="gs-keyboard-button" on-tap="dispatch" type="button" class="btn btn-secondary gs-keyboard-key">
      {{ key }}
    </button>
  </template>

  <style>
    .gs-keyboard-key {
      background-color: #ecf0f1;
      border: 1px solid #dce4ec;
      border-radius: 4px;
      cursor: pointer;
      margin-bottom: 3px;
    }

    .gs-keyboard-key {
      height: 100%;
      width: 100%;
    }

    .gs-keyboard-key:hover {
      color: unset;
    }

    .gs-keyboard-key:focus {
      outline-color: #0B456D;
    }
  </style>

  <script>
    Polymer({
      is: 'gs-keyboard-key',
      properties: {
        key: {
          type: String
        },
        keyCode: {
          type: String,
        },
        dispatchKeydown: {
          type: Object
        }
      },
      ready: function () {
        if(!this.keyCode) {
          this.keyCode = this.key;
        }
      },
      dispatch: function () {
        this.dispatchKeydown(this.keyCode)
      }
    })
  </script>
</dom-module>

<dom-module id="gs-keyboard">
  <style>
    .gs-keyboard-container {
      display: flex;
      flex-direction: row;
      justify-content: center;
      height: 100%;
    }

    .gs-keyboard-container .section {
      flex-grow: 4;
      height: 100%;
      width: 100%;
    }

    .arrow-keys {
      padding: 15px 0;
    }

    .gs-keyboard-key-container.small {
      flex-basis: 33%;
    }

    .gs-keyboard-key-container {
      flex-grow: 1;
      padding: 1px;
    }

    .gs-keyboard-keys-container {
      display: flex;
      flex-wrap: wrap;
    }

  </style>

  <template>
    <template is="dom-if" if="{{configLoaded}}">
      <div class="gs-keyboard-container">
        <div class="section gs-keyboard-keys-container">
          <template is="dom-repeat" items="{{keyboardConfig.keys}}" as="key">
            <gs-keyboard-key class="gs-keyboard-key-container small" dispatch-keydown="{{dispatchKeydown}}" key="{{key}}"></gs-keyboard-key>
          </template>
          <!-- Should definitely internationalize the space key below -->
          <gs-keyboard-key class="gs-keyboard-key-container" dispatch-keydown="{{dispatchKeydown}}" key="ESPACIO" key-code="space"></gs-keyboard-key>
        </div>
        <template is="dom-if" if="[[_shouldDisplayArrows(keyboardConfig)]]">
          <div class="section arrow-keys">
            <gs-keyboard-arrow-keys dispatch-keydown="{{dispatchKeydown}}">
            </gs-keyboard-arrow-keys>
          </div>
        </template>
      </div>
    </template>
  </template>

  <script>
    Polymer({
      is: 'gs-keyboard',
      properties: {
        keyboardConfig: {
          type: Object
        },
        dispatchKeydown: {
          type: Object,
          value: function () {
            return function (key) {
              const keyCode = GobstonesKeyboard.keyboardMap[key] || key.charCodeAt(0);
              const event = new KeyboardEvent('keydown', {'keyCode': keyCode, 'which': keyCode});
              document.dispatchEvent(event);
            };
          }
        },
        configLoaded: {
          type: Boolean,
          value: false,
          observer: '_onConfigLoaded'
        }
      },
      ready: function () {
        document.addEventListener('gs-keyboard-config-changed', this._loadConfig.bind(this));
        this._loadConfig();
      },
      _onConfigLoaded: function () {
        if(this.configLoaded) this._createSwipeListener();
      },
      _createSwipeListener: function () {
        if(typeof Hammer === "undefined") return postpone(this._createSwipeListener.bind(this));
        const $swipeListenerArea = $('.mu-initial-state gs-board')[0];
        const hammer = new Hammer($swipeListenerArea);
        hammer.get('swipe').set({direction: Hammer.DIRECTION_ALL});
        hammer.on('swipe', (event) => {
          this.dispatchKeydown(event.direction)
        });
      },
      _loadConfig: function () {
        if (typeof GobstonesKeyboard !== "undefined") {
          this.keyboardConfig = GobstonesKeyboard.config;
          this.configLoaded = true;
        }
      },
      _shouldDisplayArrows: function (keyboardConfig) {
        return keyboardConfig && keyboardConfig.showArrows;
      }
    })
  </script>
</dom-module>

<dom-module id="gs-keyboard-config">
  <script>
    Polymer({
      is: 'gs-keyboard-config',
      properties: {
        keyboardUrl: Object,
        observer: '_keyboardChanged'
      },

      attached: function () {
        this._createKeyboardConfig();
        this._setKeyboard();
      },

      _createKeyboardConfig: function () {
        if(typeof GobstonesKeyboard === 'undefined') GobstonesKeyboard = {};
        if(typeof Hammer === "undefined") return postpone(this._createKeyboardConfig.bind(this));
        GobstonesKeyboard.keyboardMap = {
          [Hammer.DIRECTION_LEFT]: 37,
          [Hammer.DIRECTION_UP]: 38,
          [Hammer.DIRECTION_RIGHT]: 39,
          [Hammer.DIRECTION_DOWN]: 40,
          'space': 32
        }
      },

      _keyboardChanged: function () {
        this._setKeyboard();
      },

      _setKeyboard: function () {
        $.getJSON(this.keyboardUrl, (keyboardConfig) => {
          GobstonesKeyboard.config = keyboardConfig;
          this.fire('gs-keyboard-config-changed');
        })
      }
    });
  </script>
</dom-module>
<commit_msg>Use setTimeout in trash relocating to ensure layout has already resized<commit_after><link href="https://fonts.googleapis.com/css?family=Nunito:700" rel="stylesheet">

<link href="./gs-element-blockly.html" rel="import"/>
<link href="./gobstones-code-runner.html" rel="import"/>
<script src="./hammer.min.js"></script>

<script>
  Blockly.FieldImage.prototype.__setValue__ = Blockly.FieldImage.prototype.setValue;
  Blockly.FieldImage.prototype.setValue = function(a) {
    if (a && a.startsWith("undefined")) {
      console.debug(`Ignoring unavailable image ${a}`);
    } else if (a) {
      const absoluteUrl = a
        .replace("../media/", "../local-media/")
        .replace("../local-media/", "<%= @assets_url %>/local-media/");
      this.__setValue__(absoluteUrl);
    } else {
      this.__setValue__(a);
    }
  }

  Blockly.FieldImage.prototype.__setTooltip__ = Blockly.FieldImage.prototype.setTooltip;
  Blockly.FieldImage.prototype.setTooltip = function(a) {
    try {
      this.__setTooltip__(a);
    } catch (_e) {
      console.debug('Could not set tooltip');
    }
  }

  function postpone(action) {
    return setTimeout(action, 50);
  }
</script>

<dom-module id="mu-gobstones-custom-editor">

  <template>
    <gs-element-blockly id="blocklyElement"
                        read-only="{{readOnly}}"
                        media="{{mediaUrl}}"
                        local-media="{{localMediaUrl}}"
    ></gs-element-blockly>
    <gs-element-blockly id="blocklyTmp"
                        style="display: none"
                        media="{{mediaUrl}}"
                        localMediaUrl="{{localMediaUrl}}"
    ></gs-element-blockly>
  </template>

  <script>
    const compilationModes = {
      gameFramework: {
        compile: ({ main, teacher, ...args }) => {
          const extra = `<%= @game_framework_extra %>`;
          const program = `<%= @game_framework_program %>`;

          const append = (code, anotherCode) => `${code}\n\n${anotherCode}`

          return {
            main: append(main, program),
            teacher: append(teacher, extra),
            ...args
          };
        },

        scrollToMainBlock: (blockly) => {
          const mainBlock = blockly.getBlocksOfType('procedures_defnoreturnnoparams')[0];
          if (mainBlock) {
            blockly.scrollToBlock(mainBlock.id);
          }
        },

        defaultCode: `<%= @game_framework_default %>`
      },
      classic: {
        compile: (code) => code,

        scrollToMainBlock: (blockly) => {
          blockly.scrollToBlock();
        },

        defaultCode: ''
      }
    };

    Polymer({
      is: 'mu-gobstones-custom-editor',
      properties: {
        mediaUrl: {
          type: String,
          value: "<%= @assets_url %>/media/"
        },
        localMediaUrl: {
          type: String,
          value: "<%= @assets_url %>/local-media/"
        },
        readOnly: {
          type: Boolean,
          value: false
        },
        teacherMode: {
          type: Boolean,
          default: false
        },
        defaultIcons: {
          type: Object,
          value: {
            procedureDeclarations: { /** for future use */ },
            functionDeclarations: { /** for future use */ }
          }
        }
      },

      // ==== Polymer events

      attached() {
        this.$exerciseContainer = $('.mu-kids-exercise');

        /*
        https://polymer-library.polymer-project.org/1.0/docs/devguide/registering-elements#initialization-order

        There are no guarantees with regard to initialization timing between sibling elements.
        This means that siblings may become ready in any order.
        For accessing sibling elements when an element initializes, you can call async from inside the attached callback.
        */
        this.async(() => {
          this._configureBlocklyAppearance();
          this._configureBlocklyBehavior();
        });
      },

      // ==== Public methods (accesible from other components)

      reset() {
        const blockly = this._getBlockly();
        blockly.workspaceXml = blockly.initialXml;
        this._scrollToMainBlock();
      },

      /**
       * Configures a custom toolbox using a toolbox url
       * This method does nothing if editor is read-only, but
       * assumes a gs-toolbox element is present - otherwise it would
       * have no sense to have a toolboxUrl.
       */
      configureCustomToolboxFromUrl(toolboxUrl) {
        if (this.readOnly) return;

        console.debug('Configuring custom toolbox');

        $.get(toolboxUrl, (toolboxXml) => {
          this._initializeCustomToolboxBlocklyWorkspace(toolboxXml);
        });
      },

      /**
       * Wether a custom toolbox is required. Read only editors and gs-toolbox-less context
       * don't require it
       */
      hasCustomToolbox() {
        return !this.readOnly && $('gs-toolbox').length;
      },

      compile(code) {
        return this._compilationMode().compile(code);
      },

      hasInteractiveProgram() {
        return this._getBlockly().initialXml.indexOf("block type=\"InteractiveProgram\"") !== -1;
      },

      toggleInteractiveMode() {
        this.$exerciseContainer.toggleClass('play-mode');
        this._triggerResize();
      },

      getTeacherCode() {
        const extraCode = this._getExtraCode();
        if (!extraCode) return;
        this.$.blocklyTmp.workspaceXml = extraCode;
        return this.$.blocklyTmp.generateCode();
      },

      getStudentCode: function () {
        return this
          ._getBlockly()
          .generateCode({withRegions: true, clearErrors: false});
      },

      getStudentXml: function () {
        return this._getStudentSolution() || "";
      },

      getTestCode() {
        return this._getEditorTest().value;
      },

      removeBlockErrors() {
        return this._getBlockly().workspace.removeBlockErrors();
      },

      showBlockError(region, error) {
        this._getBlockly().scrollToBlock(region);
        this._getBlockly().showBlockError(region, error.message);
      },

      highlightBlock(region) {
        this._getBlockly().highlightBlock(region);
      },

      // ==== Private methods (only for internal usage)

      _configureBlocklyAppearance() {
        this._getBlockly().showCategories = !this._isKindergarten();
        this._setBlocklyDisplayMode();
        this._setBlocklySounds();
        this._setBlocklyColors();
        this._relocateTrashOnResize();

        // Removing "/" from the block id character set to avoid syntax errors
        Blockly.utils.genUid.soup_ = Blockly.utils.genUid.soup_.replace("/", "");
      },

      _configureBlocklyBehavior() {
        this._setTeacherActions();
        this._setGameActions();
        this._initializeNonCustomToolboxBlocklyWorkspace();
        this._registerLayoutChangedEvent();
      },

      _relocateTrashOnResize() {
        if (this.readOnly) {
          console.debug("No trash relocate on resize required");
          return;
        }

        $('.mu-kids-context, .mu-kids-results').on('hidden.bs.modal shown.bs.modal', () => {
          this._relocateTrash();
        });

        $(window).resize(() => setTimeout(this._relocateTrash));
      },


      /**
       * Initializes a workspace using a custom toolbox.
       *
       * Blockly's workspace is destroyed when toolbox changes, so initialization
       * is performed here

       * @see _initializeBlocklyWorkspace
       */
      _initializeCustomToolboxBlocklyWorkspace(toolboxXml) {
        this._getBlockly().toolbox = { defaultToolbox: toolboxXml };
        this._initializeBlocklyWorkspace();
      },

      /**
       * Initializes a workspace using a non-custom toolbox.
       *
       * Blockly's workspace is destroyed when toolbox changes,
       * so this method will initialize it only if a there is a custom toolbox
       * that will be called later.
       *
       * @see _initializeBlocklyWorkspace
       */
      _initializeNonCustomToolboxBlocklyWorkspace() {
        if (!this.hasCustomToolbox()) {
          this._initializeBlocklyWorkspace();
        }
      },

      /**
       * Performs actual workspace (re)creation,
       * which sets initial xml, user solution, trash position
       * and appropriate callbacks.
       *
       * This method is the actual culmination of the blockly initialization
       * process
       */
      _initializeBlocklyWorkspace() {
        console.debug('Initializing Blockly Workspace');

        this._setInitialXml();
        this._initializeWorkspace();
        this._relocateTrash();
        this._subscribeToWorkspace(() => this._updateSolution());

        if (this.hasInteractiveProgram()) {
          this._setInteractiveLayout();
        }

        mumuki.assetsLoadedFor('editor');
      },

      _relocateTrash() {
        if (this.readOnly) {
          console.debug("No trash relocate required");
          return;
        }

        const $blocklyTrash = $('.blocklyTrash');
        const $blocklyDiv = $('#blocklyDiv');
        const width = $blocklyDiv.width() - 68;
        const height = $blocklyDiv.height() - 210;
        $blocklyTrash.css("transform", `translate(${width}px, ${height}px)`);
        $blocklyTrash.css("display", "unset");
      },

      _updateSolution() {
        console.debug("Updating solution");
        const blockly = this._getBlockly();
        let changed;
        if (this.teacherMode) {
          changed = this._setExtraCode(blockly.workspaceXml);
        } else {
          changed = this._setStudentSolution(blockly.workspaceXml);
        }

        if (!changed) {
          console.debug("No solution update required")
          return;
        }

        // TODO: this hack enables Angular two-way binding for Bibliotheca. Should be replaced with Mumuki events system
        if (typeof angular !== 'undefined') {
          angular.element(this._getStudentEditor()).triggerHandler("change");
          angular.element(this._getEditorExtra()).triggerHandler("change");
        }

        this._stopExecution();
      },

      _setBlocklySounds() {
        Blockly.WorkspaceAudio.prototype.preload = () => {
          for (const soundName in this.SOUNDS_) {
            const sound = this.SOUNDS_[soundName];
            sound.volume = 0.01;
            const playPromise = sound.play();
            playPromise && playPromise.then(sound.pause.bind(sound));
            if (goog.userAgent.IPAD || goog.userAgent.IPHONE) break;
          }
        };
      },

      _setBlocklyColors() {
        Blockly.HSV_SATURATION = 0.64;
        Blockly.HSV_VALUE = 1;

        Blockly.MUMUKI_COLORS = {
          pink: "#FF5C82",
          blue: "#5CBEFF",
          yellow: "#FFC95C",
          green: "#5CED71"
        };

        // reserved
        Blockly.CUSTOM_COLORS.program = Blockly.MUMUKI_COLORS.pink;
        Blockly.CUSTOM_COLORS.interactiveProgram = Blockly.MUMUKI_COLORS.pink;
        Blockly.CUSTOM_COLORS.interactiveBinding = Blockly.MUMUKI_COLORS.pink;
        Blockly.CUSTOM_COLORS.procedure = Blockly.MUMUKI_COLORS.pink;
        Blockly.CUSTOM_COLORS.function = Blockly.MUMUKI_COLORS.pink;
        Blockly.CUSTOM_COLORS.complete = Blockly.MUMUKI_COLORS.pink;

        // control structures
        Blockly.CUSTOM_COLORS.controlStructure = this._isKindergarten() ? Blockly.MUMUKI_COLORS.green : Blockly.MUMUKI_COLORS.yellow;

        // commands
        Blockly.CUSTOM_COLORS.primitiveCommand = Blockly.MUMUKI_COLORS.yellow;
        Blockly.CUSTOM_COLORS.primitiveProcedure = Blockly.MUMUKI_COLORS.yellow;
        Blockly.CUSTOM_COLORS.procedure_call = Blockly.MUMUKI_COLORS.yellow;
        Blockly.CUSTOM_COLORS.assignation = Blockly.MUMUKI_COLORS.yellow;

        // expressions
        Blockly.Msg.MATH_HUE = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.literalExpression = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.primitiveExpression = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.operator = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.variable = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.parameter = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.primitiveFunction = Blockly.MUMUKI_COLORS.blue;
        Blockly.CUSTOM_COLORS.function_call = Blockly.MUMUKI_COLORS.blue;
      },

      _setBlocklyDisplayMode() {
        // The display mode configuration could be monkey-patched here, like this:
        Blockly.displayModes.iconic.iconSize = 64;
        Blockly.displayModes.iconic.procedureDefIcon = `${this.localMediaUrl}main-procedure.png`;

        Blockly.displayMode = this._isKindergarten() ? 'iconic' : 'text';
      },

      _triggerResize() {
        const event = document.createEvent('HTMLEvents');
        event.initEvent('resize', true, false);
        document.dispatchEvent(event);
      },

      _setInteractiveLayout() {
        this.$exerciseContainer.addClass('mu-kids-interactive');
        $('.mu-final-state').html('<gs-keyboard/>');
        $('.mu-editor').append($('<kids-interactive-submit-button/>'));
        // In order to avoid triggering click on enter or space
        $('#gbsPlayButton')[0].addEventListener('focus', function () {
          this.blur();
        })
      },

      _setTeacherActions() {
        const teacherCode = this.getTeacherCode();
        if (!teacherCode) {
          return;
        }

        console.debug("Set teacher actions");
        const actions = new Parser().getActionsFromSource(teacherCode);

        this._setPrimitiveProcedures(this._withDefaultIcons(actions, 'procedureDeclarations'));
        this._setPrimitiveFunctions(this._withDefaultIcons(actions, 'functionDeclarations'));
      },

      _setGameActions() {
        if (!this._isGame()) {
          return;
        }

        console.debug("Set game actions");
        const blockly = this._getBlockly();

        this._setPrimitiveProcedures((blockly.primitiveProcedures || []).concat([
          this._gamePrimitive('ShiftUp'),
          this._gamePrimitive('ShiftDown'),
          this._gamePrimitive('ShiftLeft'),
          this._gamePrimitive('ShiftRight')
        ]));
      },

      _setPrimitiveProcedures(procedures) {
        this._setBlocklyActions("primitiveProcedures", procedures);
      },

      _setPrimitiveFunctions(functions) {
        this._setBlocklyActions("primitiveFunctions", functions);
      },

      _setBlocklyActions(key, actions) {
        try {
          this._getBlockly()[key] = actions
        } catch (e) {
          if (this._getBlockly()[key] !== actions) {
            throw e;
          }
        }
      },

      _gamePrimitive(name) {
        return { alias: 'procedureDeclaration', name, attributes: {block_icon: `<%= @assets_url %>/media/${name}.png`} };
      },

      _getBlockly() {
        return this.$.blocklyElement;
      },

      _getStudentSolution() {
        return this._getStudentEditor().value;
      },

      /**
       * Sets the student solution xml, which corresponds
       * to user content in laboratory, and default content in bibliotheca
       */
      _setStudentSolution(code) {
        if (this._getStudentEditor().value !== code) {
          console.debug(`Setting student ${code}`)
          this._getStudentEditor().value = code;
          return true;
        }
        return false;
      },

      _getDefaultCode() {
        return this._getEditorDefaultValue().value;
      },

      _getExtraCode() {
        return this._getEditorExtra().value;
      },

      _setExtraCode(code) {
        if (this._getEditorExtra().value !== code) {
          console.debug(`Setting extra ${code}`)
          this._getEditorExtra().value = code;
          return true;
        }
        return false;
      },

      _getEditorExtra: function() {
        return $("#mu-custom-editor-extra")[0] || {};
      },

      _getEditorTest: function() {
        return $("#mu-custom-editor-test")[0];
      },

      _getStudentEditor: function() {
        return $("#mu-custom-editor-value")[0] || {};
      },

      _getEditorDefaultValue: function() {
        return $("#mu-custom-editor-default-value")[0] || {};
      },

      _stopExecution() {
        const submit = $("kids-submit-button")[0];
        if (submit && submit.$.runner.isRunning) {
          submit.$.runner.stop();
        }
      },

      _registerLayoutChangedEvent() {
        if (!this.teacherMode) {
          return;
        }

        mumuki.events.on('layoutChanged', () => {
          console.debug('layout changed');
          this._setStudentSolution(this._compilationMode().defaultCode);
        });
      },

      _scrollToMainBlock() {
        this._compilationMode().scrollToMainBlock(this._getBlockly());
      },

      _withDefaultIcons(actions, type) {
        const addIcon = (declaration, block_icon) =>
          ({ ...declaration, attributes: { ...declaration.attributes, block_icon } });

        return actions[type].map((declaration) => {
          const defaultIcon = this.defaultIcons[type][declaration.name];
          if (defaultIcon) {
            return addIcon(declaration, defaultIcon);
          } else {
            return declaration;
          }
        });
      },

      _isKindergarten() {
        return mumuki.exercise.layout === 'input_kindergarten';
      },

      _isGame() {
        if (mumuki.exercise.settings) {
          return mumuki.exercise.settings.game_framework;
        } else {
          // for backwards compatibility
          return this._isKindergarten();
        }
      },

      _compilationMode() {
        return this._isGame() ? compilationModes.gameFramework : compilationModes.classic;
      },

      _setInitialXml: function () {
        const blockly = this._getBlockly();
        const defaultCode = this._getDefaultCode();
        if (defaultCode) {
          blockly.initialXml = defaultCode;
        } else {
          blockly.initialXml = blockly.workspaceXml;
        }
      },

      _initializeWorkspace() {
        const blockly = this._getBlockly();
        const value = this._getStudentSolution();

        blockly.workspaceXml = value || (
          this.teacherMode
            ? "<xml></xml>"
            : blockly.initialXml
        );

        this._scrollToMainBlock();
      },

      _subscribeToWorkspace: function(action) {
        this._getBlockly().workspace.addChangeListener(action);
      }
    });
  </script>
</dom-module>

<dom-module id="waiting-spinner">
  <style>
    .spinner {
      width: 60px;
      height: 60px;
    }

    .double-bounce1, .double-bounce2 {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background-color: #ff4081;
      opacity: 0.6;
      position: absolute;
      top: 0;
      left: 0;

      -webkit-animation: sk-bounce 2.0s infinite ease-in-out;
      animation: sk-bounce 2.0s infinite ease-in-out;
    }

    .double-bounce2 {
      -webkit-animation-delay: -1.0s;
      animation-delay: -1.0s;
    }

    @-webkit-keyframes sk-bounce {
      0%, 100% {
        -webkit-transform: scale(0.0)
      }
      50% {
        -webkit-transform: scale(1.0)
      }
    }

    @keyframes sk-bounce {
      0%, 100% {
        transform: scale(0.0);
        -webkit-transform: scale(0.0);
      }
      50% {
        transform: scale(1.0);
        -webkit-transform: scale(1.0);
      }
    }
  </style>

  <template>
    <div class="spinner">
      <div class="double-bounce1"></div>
      <div class="double-bounce2"></div>
    </div>
  </template>

  <script>
    Polymer({
      is: "waiting-spinner"
    });
  </script>
</dom-module>

<dom-module id="kids-interactive-submit-button">
  <style>
    .mu-kids-interactive-submit-button {
      position: absolute;
      right: 80px;
      width: 56px;
      height: 56px;
      bottom: 15px;
      z-index: 70;
    }

    .mu-kids-interactive-submit-button paper-fab {
      background-color: #0B456D;
    }

  </style>

  <template>
    <div class="mu-kids-interactive-submit-button">
      <paper-fab icon="av:skip-next"
                 on-click="_onButtonClick"
      ></paper-fab>
    </div>
  </template>

  <script>
    Polymer({
      is: 'kids-interactive-submit-button',

      ready: function () {
        this.submitButton = $("kids-submit-button")[0];
        this.codeRunner = this.submitButton.$.runner;
      },

      _onButtonClick: function () {
        const currentPromise = this.submitButton.serverPromise;
        if(!currentPromise) return;
        currentPromise.always(() => {
          const currentResponse = this.submitButton.serverResponse;
          this.codeRunner.stop();
          mumuki.kids.showResult(currentResponse);
          this.codeRunner.isDirty = true;
        })
      },
    });
  </script>
</dom-module>

<dom-module id="kids-reset-button">
  <template>
    <div>
      <paper-fab id="gbsResetButton"
                 icon="av:replay"
                 on-click="_onButtonClick"
      ></paper-fab>
    </div>
  </template>

  <script>
    Polymer({
      is: 'kids-reset-button',

      _onButtonClick: function () {
        this._getEditor().reset();
      },

      _getEditor: function () {
        return $("mu-gobstones-custom-editor")[0];
      }
    });
  </script>
</dom-module>

<dom-module id="kids-submit-button">
  <style>
    .hidden {
      visibility: hidden;
    }

    .visible {
      visibility: visible;
    }

    .spinner {
      position: absolute;
      top: 0;
      left: 0;
      transform: translate(-2px, -2px);
    }
  </style>

  <template>
    <div style="position: relative">
      <gobstones-code-runner id="runner" class$="{{_getSubmitCss(isWaiting)}}"></gobstones-code-runner>
      <waiting-spinner class$="spinner {{_getSpinnerCss(isWaiting)}}"></waiting-spinner>
    </div>
  </template>

  <script>
    Polymer({
      is: "kids-submit-button",
      listeners: {
        "gbs-run-request": "_onRunRequest",
        "gbs-stop": "_onStop",
        "gbs-reset-state": "_onResetState"
      },
      properties: {
        serverPromise: Object,
        serverResponse: Object,
        isWaiting: {
          type: Boolean,
          value: false
        }
      },

      ready: function () {
        const resetStatusAfterAborted = () => {
          $('.mu-kids-results-aborted').on('hidden.bs.modal', () => {
            this.$.runner.isDirty = false;
            this._onResetState();
          })
        };
        const _getScenarioSelector = function (scenario) {
          return `.mu-${scenario}-state .mu-kids-state-image > *`;
        };

        const _createMultipleScenarios = () => {
          const scenarios = ['initial', 'final'];
          const scenarioSelectors = scenarios.map(it => _getScenarioSelector(it));
          this.multipleScenarios = new mumuki.MultipleScenarios(scenarioSelectors);
        };

        const setInitialState = () => {
          const initialBoards = this._getInitialBoards();
          if (!initialBoards[0].size) return postpone(setInitialState);

          this.initialState = initialBoards.toArray().map((board) => this._boardToState(board));

          resetStatusAfterAborted();
        };

        _createMultipleScenarios();
        setInitialState();
        this.$editor = $("mu-gobstones-custom-editor")[0];
      },

      _boardToState : function ({size, table, header, boom}) {
        return {size, table, header, boom}
      },

      _onRunRequest: function (event) {
        const {detail: controller} = event;

        const editor = this.$editor;
        const xml = editor.getStudentXml();
        const code = editor.getStudentCode();
        const teacherCode = editor.getTeacherCode() || "";
        const finalBoard = this._getTargetBoard();
        const solution = {content: xml};
        const executionSpeed = 2;

        this._cleanState();
        this._cleanErrors(finalBoard);

        const targetStateIndex = this.multipleScenarios.currentScenarioIndex;
        this.targetState = this.initialState[targetStateIndex];

        const promise = new mumuki.bridge.Laboratory()
          .runTests(solution)
          .then((results) => {
            if (promise !== this.serverPromise) return;

            this._onRemoteExecutionStop(results);
          })
          .catch(() => {
            mumuki.kids.showResult({status: 'aborted'})
          });
        this.serverPromise = promise;

        editor.toggleInteractiveMode();

        controller.start({
          initialState: this.targetState,
          code: editor.compile({
            main: code,
            library: "",
            teacher: teacherCode
          })
        }, {
          onResult: (state, fullState) => {
            const region = this._getLastRegion(fullState);
            if (region) this._highlight(region);

            this._updateBoard(state, finalBoard);
          },
          onCompilationError: (error) => {
            const {region} = error.on;
            if (region) this._showError(region, error);
          }
        }, executionSpeed);
      },

      _onStop: function (event) {
        const reason = event.detail;

        const editor = this.$editor;

        if (reason === "end") {
          this._onExecutionEnd(editor);
        }
        else if (reason === "cancel" && editor.interactiveMode) {
          this._onInteractiveStop(editor);
        }
        else this.serverPromise = undefined;
      },

      _onExecutionEnd: function (editor) {
        if(editor.interactiveMode) {
          editor.toggleInteractiveMode();
        } else {
          this._onLocalExecutionStop();
          this._toggleInitialState();
        }
        this.$.runner.isDirty = true;
      },

      _onInteractiveStop: function (editor) {
        this._removeBlockErrors();
        this._cleanState();
        this._resetBoards();
        editor.toggleInteractiveMode();
      },

      _onLocalExecutionStop: function () {
        if (!this.serverResponse) this.isWaiting = true;
        else this._onExecutionStop(this.serverResponse);
      },

      _onRemoteExecutionStop: function (serverResponse) {
        this.serverResponse = serverResponse;
        if (!this.$.runner.isRunning && !this.$editor.interactiveMode) {
          this._onExecutionStop(serverResponse);
        }
      },

      _onExecutionStop: function (data) {
        this._cleanState();
        this._setResultsBoards(data.test_results);
        mumuki.kids.showResult(data);
      },

      _setResultsBoards: function (testResults) {
        if (!testResults) return;
        const testResultsBoards = this._testResultsBoards(testResults);
        this._updateBoards(testResultsBoards);
        const testResultsStatuses = this._testResultsStatuses(testResults);
        this.multipleScenarios.updateIndicators(testResultsStatuses);
      },

      _testResultsBoards: function (testResults) {
        return testResults.map(it => $(it.result).find('.actual_board > *, .final_board > *')[0]);
      },

      _testResultsStatuses: function (testResults) {
        return testResults.map(it => it.status);
      },

      _updateBoards: function (resultsBoards) {
        const initialBoards = this.multipleScenarios.scenarios[0];
        resultsBoards.forEach((resultScenario, index) => {
          if (this.multipleScenarios.currentScenarioIndex === index) return;
          const currentBoard = initialBoards[index];
          this._updateBoardFromResult(currentBoard.firstChild, resultScenario);
        });
      },

      _updateBoardFromResult: function (originalBoard, resultBoard) {
        // In Firefox the gbb is present in the textContent attribute while in Chrome it's in the gbb attribute.
        const resultGbb = resultBoard.textContent || resultBoard.gbb;
        if (resultGbb) {
          const boardAttrs = gbbReader.fromString(resultGbb);
          originalBoard.update(boardAttrs.table, boardAttrs.head);
        }
        else {
          resultBoard = resultBoard.cloneNode(); // To force initialization
          originalBoard.boom = resultBoard.boom;
        }
      },

      _updateBoard: function (state, finalBoard) {
        const {error, table, head} = state;

        finalBoard.boom = error != null;

        if (error) {
          const region = this._getLastRegion(error.on);
          if (region) this._showError(region, error);
        } else {
          finalBoard.update(table, head);
        }

        this._removeBorder(finalBoard);
      },

      _removeBorder(board) {
        board._setBorderOff();
        board.updateStyles();
      },

      _highlight: function (region) {
        this.$editor.highlightBlock(region);
      },

      _showError: function (region, error) {
        const expectsTimeout = /expect_endless_while *: *true/.test(
          this.$editor.getTestCode()
        );

        const isExpectedTimeout = (
          error.reason && error.reason.code === "timeout" &&
          expectsTimeout
        );

        if (isExpectedTimeout) return;

        this.$editor.showBlockError(region, error);
      },

      _cleanState: function () {
        this.serverPromise = undefined;
        this.serverResponse = undefined;
        this.isWaiting = false;
      },

      _cleanErrors: function (finalBoard) {
        this._removeBlockErrors();
        finalBoard.boom = false;
      },

      _removeBlockErrors() {
        this.$editor.removeBlockErrors();
      },

      _getLastRegion: function (context = {}) {
        const {regionStack} = context;
        return regionStack && regionStack.filter(it => it).slice(-1)[0];
      },

      _getInitialBoards: function () {
        return $(".mu-initial-state gs-board");
      },

      _getTargetBoard: function () {
        return this._getInitialBoards().parent('.active').children()[0];
      },

      _getSubmitCss: function (isWaiting) {
        return isWaiting ? "hidden" : "visible";
      },

      _getSpinnerCss: function (isWaiting) {
        return isWaiting ? "visible" : "hidden";
      },

      _resetBoards: function () {
        const boards = this._getInitialBoards();
        this.initialState.forEach(({table, header, boom}, index) => {
          const board = boards[index];
          board.update(table, header);
          board.boom = boom;
        });
      },

      _onResetState: function () {
        this._removeBlockErrors();
        this._resetBoards();
        this._toggleInitialState();
        this.multipleScenarios.resetIndicators();
        mumuki.kids.restart();
      },

      _toggleInitialState: function () {
        if(this.$editor.interactiveMode) return;
        $("#mu-initial-state-text").toggle();
        $("#mu-actual-state-text").toggle();
      }
    });
  </script>
</dom-module>

<dom-module id="gs-toolbox">
  <script>
    Polymer({
      is: 'gs-toolbox',
      properties: {
        toolboxUrl: Object,
        observer: '_setEditorToolbox'
      },

      attached: function () {
        this.async(() => {
          this._setEditorToolbox();
        });
      },

      _setEditorToolbox: function () {
        const $editor = $("mu-gobstones-custom-editor")[0];

        // there is no editor to configure
        if (!$editor) return;

        $editor.configureCustomToolboxFromUrl(this.toolboxUrl);
      }
    });
  </script>
</dom-module>

<dom-module id="gs-keyboard-arrow-keys">
  <template>
    <div class="gs-keyboard-arrow-keys-container">
      <svg class="keys-svg" viewBox="0 0 100 100" height="100%">
        <path d="M50,0 60,10 60,26 40,26 40,10Z" data-direction="DIRECTION_UP" on-tap="dispatchArrowPressed"></path>
        <path d="M50,100 60,90 60,74 40,74 40,90Z" data-direction="DIRECTION_DOWN" on-tap="dispatchArrowPressed"></path>
        <path d="M0,50 10,60 26,60 26,40 10,40Z" data-direction="DIRECTION_LEFT" on-tap="dispatchArrowPressed"></path>
        <path d="M100,50 90,60 74,60 74,40 90,40Z" data-direction="DIRECTION_RIGHT" on-tap="dispatchArrowPressed"></path>
      </svg>
    </div>
  </template>

  <style>
    .gs-keyboard-arrow-keys path {
      fill: #0B456D;
      cursor: pointer;
    }

    .gs-keyboard-arrow-keys-container {
      height: 100%;
      width: 100%;
      position: relative;
    }

    .keys-svg {
      display: block;
      margin: 0 auto;
      padding-left: 10px;
      overflow: visible;
    }

    .keys-svg path {
      filter: drop-shadow(2px 2px 2px rgba(11, 70, 93, .4));
      -webkit-filter: drop-shadow(2px 2px 2px rgba(11, 70, 93, .4));
    }

    .keys-svg path.pressed {
      filter: drop-shadow(1px 1px 1px rgba(11, 70, 93, .4));
      -webkit-filter: drop-shadow(1px 1px 1px rgba(11, 70, 93, .4));
      transform: translate(1px, 1px);
    }
  </style>

  <script>
    Polymer({
      is: 'gs-keyboard-arrow-keys',
      properties: {
        dispatchKeydown: {
          type: Object
        }
      },
      ready: function () {
      },
      clickTransition: function (direction) {
        const arrow = $(this).find('path[data-direction=' + direction +']');
        arrow.toggleClass('pressed');
        setTimeout(() => arrow.toggleClass('pressed'), 300);
      },
      dispatchArrowPressed: function (event) {
        const direction = event.target.dataset.direction;
        this.dispatchKeydown(Hammer[direction]);
        this.clickTransition(direction);
      }
    })
  </script>
</dom-module>

<dom-module id="gs-keyboard-key">
  <template>
    <button class="gs-keyboard-button" on-tap="dispatch" type="button" class="btn btn-secondary gs-keyboard-key">
      {{ key }}
    </button>
  </template>

  <style>
    .gs-keyboard-key {
      background-color: #ecf0f1;
      border: 1px solid #dce4ec;
      border-radius: 4px;
      cursor: pointer;
      margin-bottom: 3px;
    }

    .gs-keyboard-key {
      height: 100%;
      width: 100%;
    }

    .gs-keyboard-key:hover {
      color: unset;
    }

    .gs-keyboard-key:focus {
      outline-color: #0B456D;
    }
  </style>

  <script>
    Polymer({
      is: 'gs-keyboard-key',
      properties: {
        key: {
          type: String
        },
        keyCode: {
          type: String,
        },
        dispatchKeydown: {
          type: Object
        }
      },
      ready: function () {
        if(!this.keyCode) {
          this.keyCode = this.key;
        }
      },
      dispatch: function () {
        this.dispatchKeydown(this.keyCode)
      }
    })
  </script>
</dom-module>

<dom-module id="gs-keyboard">
  <style>
    .gs-keyboard-container {
      display: flex;
      flex-direction: row;
      justify-content: center;
      height: 100%;
    }

    .gs-keyboard-container .section {
      flex-grow: 4;
      height: 100%;
      width: 100%;
    }

    .arrow-keys {
      padding: 15px 0;
    }

    .gs-keyboard-key-container.small {
      flex-basis: 33%;
    }

    .gs-keyboard-key-container {
      flex-grow: 1;
      padding: 1px;
    }

    .gs-keyboard-keys-container {
      display: flex;
      flex-wrap: wrap;
    }

  </style>

  <template>
    <template is="dom-if" if="{{configLoaded}}">
      <div class="gs-keyboard-container">
        <div class="section gs-keyboard-keys-container">
          <template is="dom-repeat" items="{{keyboardConfig.keys}}" as="key">
            <gs-keyboard-key class="gs-keyboard-key-container small" dispatch-keydown="{{dispatchKeydown}}" key="{{key}}"></gs-keyboard-key>
          </template>
          <!-- Should definitely internationalize the space key below -->
          <gs-keyboard-key class="gs-keyboard-key-container" dispatch-keydown="{{dispatchKeydown}}" key="ESPACIO" key-code="space"></gs-keyboard-key>
        </div>
        <template is="dom-if" if="[[_shouldDisplayArrows(keyboardConfig)]]">
          <div class="section arrow-keys">
            <gs-keyboard-arrow-keys dispatch-keydown="{{dispatchKeydown}}">
            </gs-keyboard-arrow-keys>
          </div>
        </template>
      </div>
    </template>
  </template>

  <script>
    Polymer({
      is: 'gs-keyboard',
      properties: {
        keyboardConfig: {
          type: Object
        },
        dispatchKeydown: {
          type: Object,
          value: function () {
            return function (key) {
              const keyCode = GobstonesKeyboard.keyboardMap[key] || key.charCodeAt(0);
              const event = new KeyboardEvent('keydown', {'keyCode': keyCode, 'which': keyCode});
              document.dispatchEvent(event);
            };
          }
        },
        configLoaded: {
          type: Boolean,
          value: false,
          observer: '_onConfigLoaded'
        }
      },
      ready: function () {
        document.addEventListener('gs-keyboard-config-changed', this._loadConfig.bind(this));
        this._loadConfig();
      },
      _onConfigLoaded: function () {
        if(this.configLoaded) this._createSwipeListener();
      },
      _createSwipeListener: function () {
        if(typeof Hammer === "undefined") return postpone(this._createSwipeListener.bind(this));
        const $swipeListenerArea = $('.mu-initial-state gs-board')[0];
        const hammer = new Hammer($swipeListenerArea);
        hammer.get('swipe').set({direction: Hammer.DIRECTION_ALL});
        hammer.on('swipe', (event) => {
          this.dispatchKeydown(event.direction)
        });
      },
      _loadConfig: function () {
        if (typeof GobstonesKeyboard !== "undefined") {
          this.keyboardConfig = GobstonesKeyboard.config;
          this.configLoaded = true;
        }
      },
      _shouldDisplayArrows: function (keyboardConfig) {
        return keyboardConfig && keyboardConfig.showArrows;
      }
    })
  </script>
</dom-module>

<dom-module id="gs-keyboard-config">
  <script>
    Polymer({
      is: 'gs-keyboard-config',
      properties: {
        keyboardUrl: Object,
        observer: '_keyboardChanged'
      },

      attached: function () {
        this._createKeyboardConfig();
        this._setKeyboard();
      },

      _createKeyboardConfig: function () {
        if(typeof GobstonesKeyboard === 'undefined') GobstonesKeyboard = {};
        if(typeof Hammer === "undefined") return postpone(this._createKeyboardConfig.bind(this));
        GobstonesKeyboard.keyboardMap = {
          [Hammer.DIRECTION_LEFT]: 37,
          [Hammer.DIRECTION_UP]: 38,
          [Hammer.DIRECTION_RIGHT]: 39,
          [Hammer.DIRECTION_DOWN]: 40,
          'space': 32
        }
      },

      _keyboardChanged: function () {
        this._setKeyboard();
      },

      _setKeyboard: function () {
        $.getJSON(this.keyboardUrl, (keyboardConfig) => {
          GobstonesKeyboard.config = keyboardConfig;
          this.fire('gs-keyboard-config-changed');
        })
      }
    });
  </script>
</dom-module>

{'fraction_non_alphanumeric': 0.1331034660881244, 'fraction_numerical': 0.008213234154656749, 'mean_word_length': 2.160822890730234, 'pattern_counts': {'":': 6, '<': 289, '<?xml version=': 0, '>': 363, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 12}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '106483', 'n_tokens_mistral': 23325, 'n_tokens_neox': 22452, 'n_words': 3977}