text: /*global H5P*/
  H5P.Blanks = (function ($, Question) {
    /**
     * @constant
     * @default
     */
    var STATE_ONGOING = 'ongoing';
    var STATE_CHECKING = 'checking';
    var STATE_SHOWING_SOLUTION = 'showing-solution';
    var STATE_FINISHED = 'finished';
  
    const XAPI_ALTERNATIVE_EXTENSION = 'https://h5p.org/x-api/alternatives';
    const XAPI_CASE_SENSITIVITY = 'https://h5p.org/x-api/case-sensitivity';
    const XAPI_REPORTING_VERSION_EXTENSION = 'https://h5p.org/x-api/h5p-reporting-version';
  
    /**
     * @typedef {Object} Params
     *  Parameters/configuration object for Blanks
     *
     * @property {Object} Params.behaviour
     * @property {string} Params.behaviour.confirmRetryDialog
     * @property {string} Params.behaviour.confirmCheckDialog
     *
     * @property {Object} Params.confirmRetry
     * @property {string} Params.confirmRetry.header
     * @property {string} Params.confirmRetry.body
     * @property {string} Params.confirmRetry.cancelLabel
     * @property {string} Params.confirmRetry.confirmLabel
     *
     * @property {Object} Params.confirmCheck
     * @property {string} Params.confirmCheck.header
     * @property {string} Params.confirmCheck.body
     * @property {string} Params.confirmCheck.cancelLabel
     * @property {string} Params.confirmCheck.confirmLabel
     */
  
    /**
     * Initialize module.
     *
     * @class H5P.Blanks
     * @extends H5P.Question
     * @param {Params} params
     * @param {number} id Content identification
     * @param {Object} contentData Task specific content data
     */
    function Blanks(params, id, contentData) {
      var self = this;
  
      // Inheritance
      Question.call(self, 'blanks');
  
      // IDs
      this.contentId = id;
      this.contentData = contentData;
  
      this.params = $.extend(true, {}, {
        text: "Fill in",
        questions: [
          "Oslo is the capital of *Norway*."
        ],
        overallFeedback: [],
        userAnswers: [], // TODO This isn't in semantics?
        showSolutions: "Show solution",
        tryAgain: "Try again",
        checkAnswer: "Check",
        changeAnswer: "Change answer",
        notFilledOut: "Please fill in all blanks to view solution",
        answerIsCorrect: "':ans' is correct",
        answerIsWrong: "':ans' is wrong",
        answeredCorrectly: "Answered correctly",
        answeredIncorrectly: "Answered incorrectly",
        solutionLabel: "Correct answer:",
        inputLabel: "Blank input @num of @total",
        inputHasTipLabel: "Tip available",
        tipLabel: "Tip",
        scoreBarLabel: 'You got :num out of :total points',
        behaviour: {
          renderDropdown: false,
          enableRetry: true,
          enableSolutionsButton: true,
          enableCheckButton: true,
          caseSensitive: true,
          showSolutionsRequiresInput: true,
          autoCheck: false,
          separateLines: false
        },
        a11yCheck: 'Check the answers. The responses will be marked as correct, incorrect, or unanswered.',
        a11yShowSolution: 'Show the solution. The task will be marked with its correct solution.',
        a11yRetry: 'Retry the task. Reset all responses and start the task over again.',
        a11yHeader: 'Checking mode',
      }, params);
  
      // Delete empty questions
      for (var i = this.params.questions.length - 1; i >= 0; i--) {
        if (!this.params.questions[i]) {
          this.params.questions.splice(i, 1);
        }
      }
  
      // Previous state
      this.contentData = contentData;
      if (this.contentData !== undefined && this.contentData.previousState !== undefined) {
        this.previousState = this.contentData.previousState;
      }
  
      // Clozes
      this.clozes = [];
  
      // Keep track tabbing forward or backwards
      this.shiftPressed = false;
  
      H5P.$body.keydown(function (event) {
        if (event.keyCode === 16) {
          self.shiftPressed = true;
        }
      }).keyup(function (event) {
        if (event.keyCode === 16) {
          self.shiftPressed = false;
        }
      });
    }
  
    // Inheritance
    Blanks.prototype = Object.create(Question.prototype);
    Blanks.prototype.constructor = Blanks;
  
    /**
     * Registers this question type's DOM elements before they are attached.
     * Called from H5P.Question.
     */
    Blanks.prototype.registerDomElements = function () {
      var self = this;
  
      // Check for task media
      var media = self.params.media;
      if (media && media.type && media.type.library) {
        media = media.type;
        var type = media.library.split(' ')[0];
        if (type === 'H5P.Image') {
          if (media.params.file) {
            // Register task image
            self.setImage(media.params.file.path, {
              disableImageZooming: self.params.media.disableImageZooming || false,
              alt: media.params.alt,
              title: media.params.title
            });
          }
        }
        else if (type === 'H5P.Video') {
          if (media.params.sources) {
            // Register task video
            self.setVideo(media);
          }
        }
        else if (type == 'H5P.Audio' && media.params.files?.length > 0) { 
          self.setAudio(self.contentId, media.params);
        }
      }
  
      // Using instructions as label for our text groups
      const labelId = 'h5p-blanks-instructions-' + Blanks.idCounter;
  
      // Register task introduction text
      self.setIntroduction('<div id="' + labelId + '">' + self.params.text + '</div>');
  
      // Register task content area
      self.setContent(self.createQuestions(labelId), {
        'class': self.params.behaviour.separateLines ? 'h5p-separate-lines' : ''
      });
  
      // ... and buttons
      self.registerButtons();
  
      // Restore previous state
      self.setH5PUserState();
    };
  
    /**
     * Create all the buttons for the task
     */
    Blanks.prototype.registerButtons = function () {
      var self = this;
  
      var $content = $('[data-content-id="' + self.contentId + '"].h5p-content');
      var $containerParents = $content.parents('.h5p-container');
  
      // select find container to attach dialogs to
      var $container;
      if ($containerParents.length !== 0) {
        // use parent highest up if any
        $container = $containerParents.last();
      }
      else if ($content.length !== 0) {
        $container = $content;
      }
      else  {
        $container = $(document.body);
      }
  
      if (!self.params.behaviour.autoCheck && this.params.behaviour.enableCheckButton) {
        // Check answer button
        self.addButton('check-answer', self.params.checkAnswer, function () {
          // Move focus to top of content
          self.a11yHeader.innerHTML = self.params.a11yHeader;
          self.a11yHeader.focus();
  
          self.toggleButtonVisibility(STATE_CHECKING);
          self.markResults();
          self.showEvaluation();
          self.triggerAnswered();
          if (self.params.behaviour.enableTranscript)
            self.showButton('show-transcript');
        }, true, {
          'aria-label': self.params.a11yCheck,
        }, {
          confirmationDialog: {
            enable: self.params.behaviour.confirmCheckDialog,
            l10n: self.params.confirmCheck,
            instance: self,
            $parentElement: $container
          }
        });
      }
  
      // Show solution button
      self.addButton('show-solution', self.params.showSolutions, function () {
        self.showCorrectAnswers(false);
      }, self.params.behaviour.enableSolutionsButton, {
        'aria-label': self.params.a11yShowSolution,
      });
  
      // Try again button
      if (self.params.behaviour.enableRetry === true) {
        self.addButton('try-again', self.params.tryAgain, function () {
          self.a11yHeader.innerHTML = '';
          self.resetTask();
          self.$questions.filter(':first').find('input:first').focus();
        }, true, {
          'aria-label': self.params.a11yRetry,
        }, {
          confirmationDialog: {
            enable: self.params.behaviour.confirmRetryDialog,
            l10n: self.params.confirmRetry,
            instance: self,
            $parentElement: $container
          }
        });
      }
  
      if (self.params.behaviour.enableTranscript){
        self.addButton('show-transcript', 'Transcript', function () {
          self.showTranscript();
          self.hideButton('show-transcript');
        }, false, {
          'aria-label': 'Transcript',
        });
      }
  
      self.toggleButtonVisibility(STATE_ONGOING);
    };
  
    /**
     * Find blanks in a string and run a handler on those blanks
     *
     * @param {string} question
     *   Question text containing blanks enclosed in asterisks.
     * @param {function} handler
     *   Replaces the blanks text with an input field.
     * @returns {string}
     *   The question with blanks replaced by the given handler.
     */
    Blanks.prototype.handleBlanks = function (question, handler) {
      // Go through the text and run handler on all asterisk
      var clozeEnd, clozeStart = question.indexOf('*');
      var self = this;
      while (clozeStart !== -1 && clozeEnd !== -1) {
        clozeStart++;
        clozeEnd = question.indexOf('*', clozeStart);
        if (clozeEnd === -1) {
          continue; // No end
        }
        var clozeContent = question.substring(clozeStart, clozeEnd);
        var replacer = '';
        if (clozeContent.length) {
          replacer = handler(self.parseSolution(clozeContent));
          clozeEnd++;
        }
        else {
          clozeStart += 1;
        }
        question = question.slice(0, clozeStart - 1) + replacer + question.slice(clozeEnd);
        clozeEnd -= clozeEnd - clozeStart - replacer.length;
  
        // Find the next cloze
        clozeStart = question.indexOf('*', clozeEnd);
      }
      return question;
    };
  
    /**
     * Create questitons html for DOM
     */
    Blanks.prototype.createQuestions = function (labelId) {
      var self = this;
  
      var html = '';
      var clozeNumber = 0;
      for (var i = 0; i < self.params.questions.length; i++) {
        var question = self.params.questions[i];
  
        // Go through the question text and replace all the asterisks with input fields
        question = self.handleBlanks(question, function (solution) {
          // Create new cloze
          clozeNumber += 1;
          var defaultUserAnswer = (self.params.userAnswers.length > self.clozes.length ? self.params.userAnswers[self.clozes.length] : null);
          var l10n = {
            answeredCorrectly: self.params.answeredCorrectly,
            answeredIncorrectly: self.params.answeredIncorrectly,
            solutionLabel: self.params.solutionLabel,
            inputLabel: self.params.inputLabel,
            inputHasTipLabel: self.params.inputHasTipLabel,
            tipLabel: self.params.tipLabel
          };
          var cloze = self.params.behaviour.renderDropdown 
            ? new Blanks.Dropdown(solution, self.params.behaviour, defaultUserAnswer, l10n) 
            : new Blanks.TextInput(solution, self.params.behaviour, defaultUserAnswer, l10n);
  
          self.clozes.push(cloze);
          return cloze;
        });
  
        html += '<div role="group" aria-labelledby="' + labelId + '">' + question + '</div>';
      }
  
      self.hasClozes = clozeNumber > 0;
      this.$questions = $(html);
  
      self.a11yHeader = document.createElement('div');
      self.a11yHeader.classList.add('hidden-but-read');
      self.a11yHeader.tabIndex = -1;
      self.$questions[0].insertBefore(self.a11yHeader, this.$questions[0].childNodes[0] || null);
  
      // Set input fields.
      this.$questions.find(self.params.behaviour.renderDropdown ? 'select' : 'input').each(function (i) {
        var afterCheck;
        if (self.params.behaviour.autoCheck) {
          afterCheck = function () {
            var answer = $("<div>").text(this.getUserAnswer()).html();
            self.read((this.correct() ? self.params.answerIsCorrect : self.params.answerIsWrong).replace(':ans', answer));
            if (self.done || self.allBlanksFilledOut()) {
              // All answers has been given. Show solutions button.
              self.toggleButtonVisibility(STATE_CHECKING);
              self.showEvaluation();
              self.triggerAnswered();
              self.done = true;
            }
          };
        }
        self.clozes[i].setInput($(this), afterCheck, function () {
          self.toggleButtonVisibility(STATE_ONGOING);
          if (!self.params.behaviour.autoCheck) {
            self.hideEvaluation();
          }
        }, i, self.clozes.length);
      }).keydown(function (event) {
        var $this = $(this);
  
        // Adjust width of text input field to match value
        self.autoGrowTextField($this);
  
        var $inputs, isLastInput;
        var enterPressed = (event.keyCode === 13);
        var tabPressedAutoCheck = (event.keyCode === 9 && self.params.behaviour.autoCheck);
  
        if (enterPressed || tabPressedAutoCheck) {
          // Figure out which inputs are left to answer
          $inputs = self.$questions.find('.h5p-input-wrapper:not(.h5p-correct) .h5p-text-input');
  
          // Figure out if this is the last input
          isLastInput = $this.is($inputs[$inputs.length - 1]);
        }
  
        if ((tabPressedAutoCheck && isLastInput && !self.shiftPressed) ||
            (enterPressed && isLastInput)) {
          // Focus first button on next tick
          setTimeout(function () {
            self.focusButton();
          }, 10);
        }
  
        if (enterPressed) {
          if (isLastInput) {
            // Check answers
            $this.trigger('blur');
          }
          else {
            // Find next input to focus
            $inputs.eq($inputs.index($this) + 1).focus();
          }
  
          return false; // Prevent form submission on enter key
        }
      }).on('change', function () {
        self.answered = true;
        self.triggerXAPI('interacted');
      });
  
      self.on('resize', function () {
        self.resetGrowTextField();
      });
  
      return this.$questions;
    };
  
    /**
     *
     */
    Blanks.prototype.autoGrowTextField = function ($input) {
      // Do not set text field size when separate lines is enabled
      if (this.params.behaviour.separateLines) {
        return;
      }
  
      var self = this;
      var fontSize = parseInt($input.css('font-size'), 10);
      var minEm = 3;
      var minPx = fontSize * minEm;
      var rightPadEm = 3.25;
      var rightPadPx = fontSize * rightPadEm;
      var static_min_pad = 0.5 * fontSize;
  
      setTimeout(function () {
        var tmp = $('<div>', {
          'text': $input.val()
        });
        tmp.css({
          'position': 'absolute',
          'white-space': 'nowrap',
          'font-size': $input.css('font-size'),
          'font-family': $input.css('font-family'),
          'padding': $input.css('padding'),
          'width': 'initial'
        });
        $input.parent().append(tmp);
        var width = tmp.width();
        var parentWidth = self.$questions.width();
        tmp.remove();
        if (width <= minPx) {
          // Apply min width
          $input.width(minPx + static_min_pad);
        }
        else if (width + rightPadPx >= parentWidth) {
          // Apply max width of parent
          $input.width(parentWidth - rightPadPx);
        }
        else {
          // Apply width that wraps input
          $input.width(width + static_min_pad);
        }
      }, 1);
    };
  
    /**
     * Resize all text field growth to current size.
     */
    Blanks.prototype.resetGrowTextField = function () {
      var self = this;
  
      this.$questions.find('input').each(function () {
        self.autoGrowTextField($(this));
      });
    };
  
    /**
     * Toggle buttons dependent of state.
     *
     * Using CSS-rules to conditionally show/hide using the data-attribute [data-state]
     */
    Blanks.prototype.toggleButtonVisibility = function (state) {
      // The show solutions button is hidden if all answers are correct
      var allCorrect = (this.getScore() === this.getMaxScore());
      if (this.params.behaviour.autoCheck && allCorrect) {
        // We are viewing the solutions
        state = STATE_FINISHED;
      }
  
      if (this.params.behaviour.enableSolutionsButton) {
        if (state === STATE_CHECKING && !allCorrect) {
          this.showButton('show-solution');
        }
        else {
          this.hideButton('show-solution');
        }
      }
  
      if (this.params.behaviour.enableRetry) {
        if ((state === STATE_CHECKING && !allCorrect) || state === STATE_SHOWING_SOLUTION) {
          this.showButton('try-again');
        }
        else {
          this.hideButton('try-again');
        }
      }
  
      if (state === STATE_ONGOING) {
        this.showButton('check-answer');
      }
      else {
        this.hideButton('check-answer');
      }
  
      this.trigger('resize');
    };
  
    /**
     * Check if solution is allowed. Warn user if not
     */
    Blanks.prototype.allowSolution = function () {
      if (this.params.behaviour.showSolutionsRequiresInput === true) {
        if (!this.allBlanksFilledOut()) {
          this.updateFeedbackContent(this.params.notFilledOut);
          this.read(this.params.notFilledOut);
          return false;
        }
      }
      return true;
    };
  
    /**
     * Check if all blanks are filled out
     *
     * @method allBlanksFilledOut
     * @return {boolean} Returns true if all blanks are filled out.
     */
    Blanks.prototype.allBlanksFilledOut = function () {
      return !this.clozes.some(function (cloze) {
        return !cloze.filledOut();
      });
    };
  
    /**
     * Mark which answers are correct and which are wrong and disable fields if retry is off.
     */
    Blanks.prototype.markResults = function () {
      var self = this;
      for (var i = 0; i < self.clozes.length; i++) {
        self.clozes[i].checkAnswer();
        if (!self.params.behaviour.enableRetry) {
          self.clozes[i].disableInput();
        }
      }
      this.trigger('resize');
    };
  
    /**
     * Removed marked results
     */
    Blanks.prototype.removeMarkedResults = function () {
      this.$questions.find('.h5p-input-wrapper').removeClass('h5p-correct h5p-wrong');
      this.$questions.find('.h5p-input-wrapper > input').attr('disabled', false);
      this.trigger('resize');
    };
  
  
    /**
     * Displays the correct answers
     * @param {boolean} [alwaysShowSolution]
     *  Will always show solution if true
     */
    Blanks.prototype.showCorrectAnswers = function (alwaysShowSolution) {
      if (!alwaysShowSolution && !this.allowSolution()) {
        return;
      }
  
      this.toggleButtonVisibility(STATE_SHOWING_SOLUTION);
      this.hideSolutions();
  
      for (var i = 0; i < this.clozes.length; i++) {
        this.clozes[i].showSolution();
      }
      this.trigger('resize');
    };
  
    /**
     * Toggle input allowed for all input fields
     *
     * @method function
     * @param  {boolean} enabled True if fields should allow input, otherwise false
     */
    Blanks.prototype.toggleAllInputs = function (enabled) {
      for (var i = 0; i < this.clozes.length; i++) {
        this.clozes[i].toggleInput(enabled);
      }
    };
  
    /**
     * Display the correct solution for the input boxes.
     *
     * This is invoked from CP and QS - be carefull!
     */
    Blanks.prototype.showSolutions = function () {
      this.params.behaviour.enableSolutionsButton = true;
      this.toggleButtonVisibility(STATE_FINISHED);
      this.markResults();
      this.showEvaluation();
      this.showCorrectAnswers(true);
      this.toggleAllInputs(false);
      //Hides all buttons in "show solution" mode.
      this.hideButtons();
    };
  
    /**
     * Resets the complete task.
     * Used in contracts.
     * @public
     */
    Blanks.prototype.resetTask = function () {
      this.answered = false;
      this.hideEvaluation();
      this.hideSolutions();
      this.clearAnswers();
      this.removeMarkedResults();
      this.toggleButtonVisibility(STATE_ONGOING);
      this.resetGrowTextField();
      this.toggleAllInputs(true);
      this.hideTranscript();
      this.hideButton('show-transcript');
      this.done = false;
    };
  
    /**
     * Hides all buttons.
     * @public
     */
    Blanks.prototype.hideButtons = function () {
      this.toggleButtonVisibility(STATE_FINISHED);
    };
  
    /**
     * Trigger xAPI answered event
     */
    Blanks.prototype.triggerAnswered = function () {
      this.answered = true;
      var xAPIEvent = this.createXAPIEventTemplate('answered');
      this.addQuestionToXAPI(xAPIEvent);
      this.addResponseToXAPI(xAPIEvent);
      this.trigger(xAPIEvent);
    };
  
    /**
     * Get xAPI data.
     * Contract used by report rendering engine.
     *
     * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
     */
    Blanks.prototype.getXAPIData = function () {
      var xAPIEvent = this.createXAPIEventTemplate('answered');
      this.addQuestionToXAPI(xAPIEvent);
      this.addResponseToXAPI(xAPIEvent);
      return {
        statement: xAPIEvent.data.statement
      };
    };
  
    /**
     * Generate xAPI object definition used in xAPI statements.
     * @return {Object}
     */
    Blanks.prototype.getxAPIDefinition = function () {
      var definition = {};
      definition.description = {
        'en-US': this.params.text
      };
      definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction';
      definition.interactionType = 'fill-in';
  
      const clozeSolutions = [];
      let crp = '';
      // xAPI forces us to create solution patterns for all possible solution combinations
      for (var i = 0; i < this.params.questions.length; i++) {
        var question = this.handleBlanks(this.params.questions[i], function (solution) {
          // Collect all solution combinations for the H5P Alternative extension
          clozeSolutions.push(solution.solutions);
  
          // Create a basic response pattern out of the first alternative for each blanks field
          crp += (!crp ? '' : '[,]') + solution.solutions[0];
  
          // We replace the solutions in the question with a "blank"
          return '__________';
        });
        definition.description['en-US'] += question;
      }
  
      // Set the basic response pattern (not supporting multiple alternatives for blanks)
      definition.correctResponsesPattern = [
        '{case_matters=' + this.params.behaviour.caseSensitive + '}' + crp,
      ];
  
      // Add the H5P Alternative extension which provides all the combinations of different answers
      // Reporting software will need to support this extension for alternatives to work.
      definition.extensions = definition.extensions || {};
      definition.extensions[XAPI_CASE_SENSITIVITY] = this.params.behaviour.caseSensitive;
      definition.extensions[XAPI_ALTERNATIVE_EXTENSION] = clozeSolutions;
      // Add title for h5p-php-reporting
      definition.extensions.title = this.getTitle();
      console.log(definition);
      return definition;
    };
  
    /**
     * Add the question itselt to the definition part of an xAPIEvent
     */
    Blanks.prototype.addQuestionToXAPI = function (xAPIEvent) {
      var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']);
      $.extend(true, definition, this.getxAPIDefinition());
  
      // Set reporting module version if alternative extension is used
      if (this.hasAlternatives) {
        const context = xAPIEvent.getVerifiedStatementValue(['context']);
        context.extensions = context.extensions || {};
        context.extensions[XAPI_REPORTING_VERSION_EXTENSION] = '1.1.0';
      }
    };
  
    /**
     * Parse the solution text (text between the asterisks)
     *
     * @param {string} solutionText
     * @returns {object} with the following properties
     *  - tip: the tip text for this solution, undefined if no tip
     *  - solutions: array of solution words
     */
    Blanks.prototype.parseSolution = function (solutionText) {
      var tip, solution;
  
      var tipStart = solutionText.indexOf(':');
      if (tipStart !== -1) {
        // Found tip, now extract
        tip = solutionText.slice(tipStart + 1);
        solution = solutionText.slice(0, tipStart);
      }
      else {
        solution = solutionText;
      }
  
      // Split up alternatives
      var solutions = solution.split('/');
      this.hasAlternatives = this.hasAlternatives || solutions.length > 1;
  
      // Trim solutions
      for (var i = 0; i < solutions.length; i++) {
        solutions[i] = H5P.trim(solutions[i]);
  
        //decodes html entities
        var elem = document.createElement('textarea');
        elem.innerHTML = solutions[i];
        solutions[i] = elem.value;
      }
  
      return {
        tip: tip,
        solutions: solutions
      };
    };
  
    /**
     * Add the response part to an xAPI event
     *
     * @param {H5P.XAPIEvent} xAPIEvent
     *  The xAPI event we will add a response to
     */
    Blanks.prototype.addResponseToXAPI = function (xAPIEvent) {
      xAPIEvent.setScoredResult(this.getScore(), this.getMaxScore(), this);
      xAPIEvent.data.statement.result.response = this.getxAPIResponse();
    };
  
    /**
     * Generate xAPI user response, used in xAPI statements.
     * @return {string} User answers separated by the "[,]" pattern
     */
    Blanks.prototype.getxAPIResponse = function () {
      var usersAnswers = this.getCurrentState();
      return usersAnswers.join('[,]');
    };
  
    /**
     * Show evaluation widget, i.e: 'You got x of y blanks correct'
     */
    Blanks.prototype.showEvaluation = function () {
      var maxScore = this.getMaxScore();
      var score = this.getScore();
      var scoreText = H5P.Question.determineOverallFeedback(this.params.overallFeedback, score / maxScore).replace('@score', score).replace('@total', maxScore);
  
      this.setFeedback(scoreText, score, maxScore, this.params.scoreBarLabel);
  
      if (score === maxScore) {
        this.toggleButtonVisibility(STATE_FINISHED);
      }
    };
  
    /**
     * Hide the evaluation widget
     */
    Blanks.prototype.hideEvaluation = function () {
      // Clear evaluation section.
      this.removeFeedback();
    };
  
    /**
     * Hide solutions. (/try again)
     */
    Blanks.prototype.hideSolutions = function () {
      // Clean solution from quiz
      this.$questions.find('.h5p-correct-answer').remove();
    };
  
    /**
     * Get maximum number of correct answers.
     *
     * @returns {Number} Max points
     */
    Blanks.prototype.getMaxScore = function () {
      var self = this;
      return self.clozes.length;
    };
  
    /**
     * Count the number of correct answers.
     *
     * @returns {Number} Points
     */
    Blanks.prototype.getScore = function () {
      var self = this;
      var correct = 0;
      for (var i = 0; i < self.clozes.length; i++) {
        if (self.clozes[i].correct()) {
          correct++;
        }
        self.params.userAnswers[i] = self.clozes[i].getUserAnswer();
      }
  
      return correct;
    };
  
    Blanks.prototype.getTitle = function () {
      return H5P.createTitle((this.contentData.metadata && this.contentData.metadata.title) ? this.contentData.metadata.title : 'Fill In');
    };
  
    /**
     * Clear the user's answers
     */
    Blanks.prototype.clearAnswers = function () {
      this.clozes.forEach(function (cloze) {
        cloze.setUserInput('');
        cloze.resetAriaLabel();
      });
    };
  
    /**
     * Checks if all has been answered.
     *
     * @returns {Boolean}
     */
    Blanks.prototype.getAnswerGiven = function () {
      return this.answered || !this.hasClozes;
    };
  
    /**
     * Helps set focus the given input field.
     * @param {jQuery} $input
     */
    Blanks.setFocus = function ($input) {
      setTimeout(function () {
        $input.focus();
      }, 1);
    };
  
    /**
     * Returns an object containing content of each cloze
     *
     * @returns {object} object containing content for each cloze
     */
    Blanks.prototype.getCurrentState = function () {
      var clozesContent = [];
  
      // Get user input for every cloze
      this.clozes.forEach(function (cloze) {
        clozesContent.push(cloze.getUserAnswer());
      });
      return clozesContent;
    };
  
    /**
     * Sets answers to current user state
     */
    Blanks.prototype.setH5PUserState = function () {
      var self = this;
      var isValidState = (this.previousState !== undefined &&
                          this.previousState.length &&
                          this.previousState.length === this.clozes.length);
  
      // Check that stored user state is valid
      if (!isValidState) {
        return;
      }
  
      // Set input from user state
      var hasAllClozesFilled = true;
      this.previousState.forEach(function (clozeContent, ccIndex) {
  
        // Register that an answer has been given
        if (clozeContent.length) {
          self.answered = true;
        }
  
        var cloze = self.clozes[ccIndex];
        cloze.setUserInput(clozeContent);
  
        // Handle instant feedback
        if (self.params.behaviour.autoCheck) {
          if (cloze.filledOut()) {
            cloze.checkAnswer();
          }
          else {
            hasAllClozesFilled = false;
          }
        }
      });
  
      if (self.params.behaviour.autoCheck && hasAllClozesFilled) {
        self.showEvaluation();
        self.toggleButtonVisibility(STATE_CHECKING);
      }
    };
  
    /**
     * Disables any active input. Useful for freezing the task and dis-allowing
     * modification of wrong answers.
     */
    Blanks.prototype.disableInput = function () {
      this.$questions.find(this.params.behaviour.renderDropdown ? 'select' : 'input').attr('disabled', true);
    };
  
    Blanks.idCounter = 0;
  
    return Blanks;
  })(H5P.jQuery, H5P.Question);
  
  /**
   * Static utility method for parsing H5P.Blanks qestion into a format useful
   * for creating reports.
   * 
   * Example question: 'H5P content may be edited using a *browser/web-browser:something you use every day*.'
   * 
   * Produces the following result:
   * [
   *   {
   *     type: 'text',
   *     content: 'H5P content may be edited using a '
   *   },
   *   {
   *     type: 'answer',
   *     correct: ['browser', 'web-browser']
   *   },
   *   {
   *     type: 'text',
   *     content: '.'
   *   }
   * ]
   * 
   * @param {string} question 
   */
  H5P.Blanks.parseText = function (question) {
    var blank = new H5P.Blanks({ question: question });
  
    /**
     * Parses a text into an array where words starting and ending
     * with an asterisk are separated from other text.
     * e.g ["this", "*is*", " an ", "*example*"]
     *
     * @param {string} text
     *
     * @return {string[]}
     */
    function tokenizeQuestionText(text) { 
      return text.split(/(\*.*?\*)/).filter(function (str) { 
        return str.length > 0; }
      );
    }
  
    function startsAndEndsWithAnAsterisk(str) {
      return str.substr(0,1) === '*' && str.substr(-1) === '*';
    }
  
    function replaceHtmlTags(str, value) {
      return str.replace(/<[^>]*>/g, value);
    }
  
    return tokenizeQuestionText(replaceHtmlTags(question, '')).map(function (part) {
      return startsAndEndsWithAnAsterisk(part) ? 
        ({
          type: 'answer',
          correct: blank.parseSolution(part.slice(1, -1)).solutions
        }) :
        ({
          type: 'text',
          content: part
        });
    });
  };
  