(function($, _, MathJax) {
  'use strict';

  // time between polls of responses API
  var POLLING_MILLIS = 3000;
  // time between timer rendering updates
  var TIMER_MILLIS = 250;
  // Timeout (in ms) for AJAX requests
  var REQUEST_TIMEOUT_MILLIS = (POLLING_MILLIS * 3) + 100;
  // Timeout (in ms) for the initial AJAX request to fetch responses and the current problem state.
  var INIT_REQUEST_TIMEOUT_MILLIS = REQUEST_TIMEOUT_MILLIS * 2;
  // The number of times that the responses endpoint should be polled unsuccessfully before showing
  // a warning to the user.
  var RESPONSES_ATTEMPTS_BEFORE_WARN = 1;
  // color palette for bars
  var PALETTE = [
    '#1e73ae',
    '#dddb32',
    '#2b9b2b',
    '#e1516f',
    '#08cdf1',
    '#a964d4',
    '#505050',
    '#f77b0e',
    '#472ecc',
    '#7cce40',
    '#876a16'
  ];
  // Label SVG appearance settings
  var LABEL_ANGLE = 30;
  var LABEL_ROTATE_VALUE = "rotate(" + LABEL_ANGLE + ", 0, 10)";
  // this sentinel value means no data should be shown
  var NONE_SELECTION = 'None';
  var GENERAL_ERROR_MESSAGE = 'There was an error. Please reload the page or try again later.';

  // An object that maps UI state names to the UI artifacts that should be shown when the UI is in
  // that state.
  var UiStates = {
    initial: {
      buttonVis: false,
      timerVis: false,
      spinnerVis: true,
      problemStatusText: "Loading responses..."
    },
    open: {
      buttonText: "Close problem now",
      buttonVis: true,
      buttonEnabled: true,
      timerVis: true,
      timerEnabled: true,
      spinnerVis: true,
      problemStatusText: "Problem is Open"
    },
    closed: {
      buttonText: "Open problem now",
      buttonVis: true,
      buttonEnabled: true,
      timerEnabled: false,
      spinnerVis: false,
      problemStatusText: undefined
    },
    closing: {
      buttonText: "Close problem now",
      buttonVis: true,
      buttonEnabled: false,
      timerVis: true,
      timerEnabled: false,
      spinnerVis: true,
      problemStatusText: "Problem is closing..."
    },
    unknownError: {
      buttonVis: false,
      buttonEnabled: false,
      timerVis: false,
      spinnerVis: false,
      problemWarningText: GENERAL_ERROR_MESSAGE
    }
  };
  UiStates.opening = _.assign({}, UiStates.closing, {
    buttonText: "Open problem now",
    problemStatusText: "Opening problem for responses..."
  });
  UiStates.openDelayed = _.assign({}, UiStates.open, {
    problemWarningText: "The server is taking a while to respond..."
  });
  UiStates.openTimedOut = _.assign({}, UiStates.open, {
    problemWarningText: "The server took too long to respond. Aborting the request and trying again..."
  });
  UiStates.fetchingFinal = _.assign({}, UiStates.closing, {
    problemStatusText: "Retrieving latest responses..."
  });
  UiStates.closingTimedOut = _.assign({}, UiStates.closing, {
    spinnerVis: false,
    problemStatusText: undefined,
    problemWarningText: "The server took too long to respond. Please try reloading the page."
  });
  UiStates.loadingTimedOut = _.assign({}, UiStates.closingTimedOut, {
    buttonVis: false,
    timerVis: false
  });


  function RapidResponseAsideView(runtime, element) {
    var toggleStatusUrl = runtime.handlerUrl(element, 'toggle_block_open_status');
    var responsesUrl = runtime.handlerUrl(element, 'responses');
    var $element = $(element);

    var rapidBlockResultsSel = '.rapid-response-results';
    var problemStatusBtnSel = '.problem-status-toggle';
    var buttonsRowSel = '.buttons-row';
    var timerSel = '.timer-readout';
    var problemStatusSpinnerSel = '.problem-status-spinner';
    var problemStatusTextSel = '.problem-status-text';
    var problemStatusWarningSel = '.problem-status-warning';
    var problemStatusWarningTextSel = '.warning-text';
    var numStudentsSel = '.num-students';
    var tooltipContainerSel = '.rapid-response-tooltip-container';
    var errorOverlaySel = '.rapid-response-error-overlay';
    var graphBarClass = 'bar';

    var tooltipTemplate = _.template(
      '<div class="rapid-response-tooltip">' +
      '<div class="tooltip-title"><%= title %></div>' +
      '<div class="tooltip-body">' +
      'Total: <span class="tooltip-total"><%= total %></span><br />' +
      'Percent: <span class="tooltip-percent"><%= percent %></span>' +
      '</div>' +
      '</div>'
    );
    // This will get attached to the beginning of <body /> on document load
    var $tooltipContainer;

    // default values
    var state = {
      is_open: false,
      runs: [],
      choices: [],
      counts: {},
      selectedRuns: [null],  // one per chart. null means select the latest one
      isChangingStatus: false,
      lastFetch: null,  // a moment object representing the time at last poll, to be used to diff with the run,
      timerInterval: null,
      responsesPollingTimeout: null,
      responsesAbortableRequest: null,
      responsesRequestAttemptCount: 0,
      ui: ""
    };

    // TODO: These values are guesses, maybe we want to calculate based on browser width/height? Not sure
    var ChartSettings = {
      top: 50, // space for text for upper label on y axis
      left: 50, // space for y axis
      right: 50, // space for text to flow
      bottom: 150, // space for x axis
      outerBufferWidth: 200, // pixels on the right and left
      outerTop: 100, // space between chart and top of container, should contain enough space for buttons
      innerBufferWidth: 0, // space between two charts
      numYAxisTicks: 6,
      minChartWidth: 400,
      maxChartWidth: 1300,
      minChartHeight: 500,
      maxChartHeight: 800
    };

    /**
     * Makes an AJAX request, creates a promise out of it, and returns that promise along with a
     *   method that can be invoked to abort the request.
     * @param {string} url The url to use for the request.
     * @param {Object} opts The options to pass to jQuery.ajax.
     * @returns {Object} An object that includes a promise representing the AJAX request, a method that
     *   can be used to abort the request, and a helper method to check if the request is in progress.
     */
    function makeAbortableRequest(url, opts) {
      opts = opts || {};
      opts.url = url;
      opts.method = opts.method || "GET";
      opts.timeout = opts.timeout || REQUEST_TIMEOUT_MILLIS;

      var deferred = new $.Deferred();
      var request = $.ajax(opts);
      request.then(function (result) {
        deferred.resolve(result);
      }).fail(function (jqXHR, textStatus) {
        deferred.reject(textStatus);
      });
      var promise = deferred.promise();
      var isPending = function() {
        return promise.state() === "pending";
      };
      var abort = function() {
        request.abort();
        deferred.reject();
      };
      return {promise: promise, abort: abort, isPending: isPending}
    }

    /**
     * Given the domain limits return some tick values, equally spaced out, all integers.
     * @param {number} domainMax The maximum domain value (the minimum is always 0).
     * @returns {Array} An array of integers within the domain used for tick values on the y axis
     */
    function makeIntegerTicks(domainMax) {
      var increment = Math.ceil(domainMax / ChartSettings.numYAxisTicks);
      return _.range(0, domainMax + 1, increment);
    }

    /**
     * Calculate width of a chart given the number of charts and the browser width
     *
     * @returns {number} Chart width in pixels
     */
    function calcChartWidth() {
      var browserWidth = $(window).width();
      var numCharts = state.selectedRuns.length;
      return Math.max(
        ChartSettings.minChartWidth,
        Math.min(
          ChartSettings.maxChartWidth,
          ((browserWidth - ChartSettings.outerBufferWidth) / numCharts) -
          (ChartSettings.innerBufferWidth * (numCharts - 1))
        )
      );
    }

    /**
     * Calculate height of a chart given the browser width
     *
     * @returns {number} Chart height in pixels
     */
    function calcChartHeight() {
      var browserHeight = $(window).height();
      return Math.max(
        ChartSettings.minChartHeight,
        Math.min(
          ChartSettings.maxChartHeight,
          browserHeight - ChartSettings.outerTop
        )
      );
    }

    function hasMathExpression(text) {
      return _.some([
        _.includes(text, "\("),
        _.includes(text, "\)"),
        _.includes(text, "$")
      ]);
    }
    /**
     * SVG doesn't have a capability to wrap text except for foreignObject which is not supported in IE11.
     * So we have to calculate it manually for X axis tick labels.
     * See https://bl.ocks.org/mbostock/7555321 for inspiration
     *
     * @param {selector} textSelector A D3 selector for x axis text elements
     * @param {number} barWidth The width of a bar
     * @param {string} text The text for the axis label
     */
    function wrapText(textSelector, barWidth, text) {
      textSelector.each(function() {
        // This is a g.tick item
        var root = d3.select(this);
        var rootText = root.select("text");

        var rootY = rootText.attr("y");
        var rootDy = parseFloat(rootText.attr("dy"));
        rootText.remove();
        root.select("g").remove();

        var radians = LABEL_ANGLE * Math.PI / 180;
        // yay trig
        // this value is the maximum length for the text when laid out at an angle
        // hypotenuse is divided in half since text is starting in center
        var maxTextWidth = (barWidth / Math.cos(radians)) / 2;
        var rootContainer = root.append("g").attr("transform", LABEL_ROTATE_VALUE);
        rootText = rootContainer.append("text")
          .attr("fill", "#000")
          .attr("text-anchor", "start")
          .attr("dy", rootDy + "em")
          .attr("y", rootY);

        var words = text.split(/\s+/);
        var tspan = rootText.append("tspan").attr("x", 0).attr("y", rootY).attr("dy", rootDy + "em");

        if (hasMathExpression(text)) {
          var textTrimmed = text.trim();
          /*
          if text has math expression as well as text data i.e (a) {math expression}, (b) ..
          then break text into math expression and english to print data in two lines.
          **/
          var middle =  " ";
          if (_.includes(text, "\(")) {
            middle = "\\(";
          } else if (_.includes(text, "$")) {
            middle = "$";
          }
          words = [
            textTrimmed.slice(0, textTrimmed.indexOf(middle)).trim(),
            textTrimmed.slice(textTrimmed.indexOf(middle), textTrimmed.length).trim()
          ];
        }

        var currentLine = 0;
        var lineHeight = 1.1;
        // Build the text word by word so that it breaks lines appropriately
        // and cuts off with an ellipsis if it gets too long
        words.forEach(function(word) {
          // If the input text is empty or we have exceeded two lines
          // don't do anything
          if (!word || currentLine > 1) {
            return;
          }

          var compiledText = tspan.text();
          tspan.text(compiledText + " " + word);
          if (tspan.node().getComputedTextLength() > maxTextWidth || hasMathExpression(text)) {
            // Check if the new word would go beyond the bar width boundary.

            // If this is the first word on the line we don't have a choice but to render it
            if (compiledText.length === 0) {
              return;
            }

            if (currentLine === 1) {
              // there is a maximum of two lines until we add the ellipses
              tspan.text(compiledText + "...");
              currentLine++;
              return;
            }

            // Change tspan back to its old text and create a one with the
            // word on a new line.
            currentLine++;
            tspan.text(compiledText + " ");
            tspan = rootText.append("tspan").attr("x", 0).attr("y", rootY).attr(
              "dy", ((currentLine * lineHeight) + rootDy) + "em"
            ).text(word);
          }
        });
      });
    }

    /**
     * Click handler to close this chart
     * @param {number} chartIndex The index of the chart
     */
    function closeChart(chartIndex) {
      state.selectedRuns.splice(chartIndex, 1);
      renderAll();
    }

    /**
     * Select handler to choose a different run for the chart
     * @param {number} chartIndex The index of the chart
     */
    function changeSelectedChart(chartIndex) {
      var selectedRun = this.value;
      if (selectedRun !== NONE_SELECTION) {
        selectedRun = parseInt(selectedRun);
      }
      state.selectedRuns[chartIndex] = selectedRun;
      renderAll();
    }

    /**
     * Click handler to open a new chart for comparison
     */
    function openNewChart() {
      state.selectedRuns = [state.selectedRuns[0], NONE_SELECTION];
      renderAll();
    }

    /**
     * Get a selected run id, or undefined if there are no runs
     *
     * @param {number} chartIndex Index of the chart
     * @returns {number} A run id
     */
    function getSelectedRun(chartIndex) {
      var runs = state.runs;
      var selectedRun = state.selectedRuns[chartIndex];
      if (selectedRun === null && runs.length > 0) {
        // The newest run should be the most recent one according to the info received from the server.
        selectedRun = runs[0].id;
      }
      return selectedRun;
    }

    /**
     * If there is an open problem, return a string showing how many people submitted a response for the open problem
     * and how many others who are enrolled.
     *
     * @param {number} runId The run to create the message for
     * @returns {string} The message
     */
    function makeNumStudentsMessage(runId) {
      var totalCount = state.total_counts[runId] || 0;
      var nounVerb = totalCount === 1 ? 'student has' : 'students have';
      return totalCount + ' ' + nounVerb + ' answered';
    }

    function formatTime(seconds) {
      return Math.floor(seconds / 60) + "m : " + (seconds % 60) + "s"
    }

    /**
     * Renders the container elements for the charts using D3
     */
    function renderChartContainer() {
      // Get the indexes for selected runs. This should either be [0] or [0, 1].
      var chartKeys = _.keys(state.selectedRuns);

      // D3 data join for charts. Create a container div for each chart to store graph, select element and buttons.
      var containers = d3.select(element)
        .select(rapidBlockResultsSel)
        .selectAll(".chart-container")
        .data(chartKeys);
      var newContainers = containers.enter()
        .append("div");

      // chart selection, close and compare buttons
      var selectionContainers = containers.selectAll(".selection-container");
      var newSelectionContainers = newContainers
        .append("div")
        .classed("selection-container", true);

      newSelectionContainers.append("select")
        .on('change', changeSelectedChart);

      newSelectionContainers.append("a")
        .classed("compare-responses", true)
        .text("Compare responses")
        .on("click", openNewChart);

      newSelectionContainers.append("a")
        .classed("close", true)
        .text("Close")
        .on('click', closeChart);

      var selectionRowsMerged = newSelectionContainers.merge(selectionContainers)
        .attr("style", "margin-left: " + ChartSettings.left + "px");
      selectionRowsMerged.selectAll(".compare-responses").classed("hidden", function() {
        return chartKeys.length !== 1 || state.ui !== "closed" || state.runs.length < 2;
      });
      selectionRowsMerged.selectAll(".close").classed("hidden", function() {
        return chartKeys.length === 1;
      });

      // create the chart svg container
      var newCharts = newContainers
        .append("svg")
        // The g element has a little bit of padding so the x and y axes can surround it
        .append("g").attr("class", "chart");
      // create x and y axes
      newCharts.append("g").attr("class", "xaxis");
      newCharts.append("g").attr("class", "yaxis");

      newContainers.merge(containers)
        .attr("class", "chart-container " + (chartKeys.length === 1 ? 'single-chart' : 'two-charts'))
        .each(function (index, __, charts) {
          renderChart(d3.select(charts[index]), index);
        })
        .selectAll("svg")
        .attr("width", calcChartWidth())
        .attr("height", calcChartHeight())
        .selectAll(".chart")
        .attr("transform", "translate(" + ChartSettings.left + "," + ChartSettings.top + ")")
        .select(".xaxis");

      // Remove charts if selectedRuns reduces in size.
      // We don't need to do this for all the inner elements, the remove will propagate.
      containers.exit().remove();
    }

    function renderMathHJax(chart) {
      MathJax.Hub.Register.StartupHook("End", function() {
        setTimeout(function() {
          chart.select(".xaxis").selectAll(".tick").each(function() {
            var tick = d3.select(this);
            var g = tick.select(".MathJax_SVG").select("svg");
            if (!g.empty()) {
              var node = g.node();
              g.remove();
              var newG = tick.append("g").attr("transform", LABEL_ROTATE_VALUE);
              newG.append(function () {
                var selectedNode = d3.select(node)
                  .attr("width", "18.778ex")
                  .attr("height", "3.161ex")
                  .attr("x", 25)
                  .attr("y", 9)
                  .attr("dy", "50em");
                return selectedNode.node();
              });
            }
          });
        }, 1);
      });
      MathJax.Hub.Queue(["Typeset", MathJax.Hub, chart.node()]);
    }

    //---------------------

    /**
     * Render template
     */
    function renderAll() {
      renderControls();
      renderChartContainer();
    }

    /**
     * Render the chart in the container.
     *
     * @param {Object} container D3 selector for the chart container
     * @param {number} chartIndex The index of the chart (either 0 or 1)
     */
    function renderChart(container, chartIndex) {
      var runs = state.runs;
      var choices = state.choices;
      var counts = state.counts;
      var selectedRun = getSelectedRun(chartIndex);
      var totalCounts = state.total_counts;

      var histogram = choices.map(function (item) {
        return {
          answer_id: item.answer_id,
          answer_text: item.answer_text,
          count: counts[item.answer_id][selectedRun] || 0,
          total: totalCounts[selectedRun] || 0
        }
      });

      // select the proper option and use it to filter the runs
      var select = container.select(".selection-container").select("select")
        .classed("hidden", state.runs.length === 0 || state.ui !== "closed");

      // D3 data join on runs to create a select list
      var optionData = [{ id: NONE_SELECTION }].concat(runs);
      var options = select.selectAll("option").data(optionData, function(run) {
        return run.id;
      });
      options.enter()
        .append("option")
        .merge(options)
        .attr("value", function(run) { return run.id; })
        .text(function(run) {
          var totalCount = state.total_counts[run.id] || 0;

          if (run.id === NONE_SELECTION) {
            return (chartIndex > 0) ? 'Select' : 'None';
          }
          var dateString = moment(run.created).format("MMMM D, YYYY, h:mm:ss a");

          var noun = totalCount === 1 ? 'Response' : 'Responses';
          return dateString + " ------- " + totalCount + " " + noun;
        });
      options.exit().remove();

      select.enter().merge(select).property('value', selectedRun);

      // Compute responses into information suitable for a bar graph.
      var histogramAnswerIds = _.pluck(histogram, 'answer_id');
      var histogramLookup = _.object(_.map(histogram, function(item) {
        return [item.answer_id, item];
      }));

      // Create x scale to map answer ids to bar x coordinate locations. Note that
      // histogram was previously sorted in order of the lowercase answer id.
      var innerWidth = calcChartWidth() - ChartSettings.left - ChartSettings.right - ChartSettings.innerBufferWidth;
      var x = d3.scaleBand()
        .rangeRound([0, innerWidth])
        .padding(0.1)
        .domain(histogramAnswerIds);

      // Create y scale to map response count to y coordinate for the top of the bar.
      var innerHeight = calcChartHeight() - ChartSettings.top - ChartSettings.bottom;
      var yDomainMax = d3.max(choices, function(choice) {
        return d3.max(_.keys(state.selectedRuns), function(chartIndex) {
          var runId = getSelectedRun(chartIndex);
          return counts[choice.answer_id][runId] || 0;
        });
      });

      var y = d3.scaleLinear().rangeRound([innerHeight, 0]).domain(
        // pick the maximum count so we know how high the bar chart should go
        [0, yDomainMax]
      );
      // Create a color scale similar to the x scale to provide colors for each bar
      var color = d3.scaleOrdinal(PALETTE).domain(histogramAnswerIds);

      // The D3 data join. This matches the histogram data to the rect elements
      // (there is a __data__ attribute on each rect keeping track of this). Also tell D3 to use the answer_id to make
      // this match.
      var chart = container.select(".chart");
      var bars = chart.selectAll("rect." + graphBarClass).data(histogram, function(item) {
        return item.answer_id;
      });

      // Set the position and color attributes for the bars. Note that there is a transition applied
      // for the y axis for existing bars being updated.
      bars.enter()
        // Everything in between enter() and merge(bars) applies only to new bars. This creates a new rect.
        .append("rect")
        .attr("class", "bar")
        // Set the height and y values according to the scale. This prevents weird transition behavior
        // where new bars appear to zap in out of nowhere.
        .attr("x", function(item) { return x(item.answer_id); })
        .attr("width", x.bandwidth())
        .attr("y", function(item) { return y(item.count); })
        .attr("height", function(item) {
          return innerHeight - y(item.count);
        })
        .on("mouseover", function(item) {
          var percent = '';
          if (item.total > 0) {
            // If there are no responses there should be no visible bars, but just in case
            percent = Math.round((item.count / item.total) * 100) + "%";
          }

          var templateState = {
            title: item.answer_text,
            total: item.count,
            percent: percent
          };
          $tooltipContainer.html(tooltipTemplate(templateState));
          if (hasMathExpression(item.answer_text)) {
            MathJax.Hub.Queue(["Typeset", MathJax.Hub, $tooltipContainer.el]);
          }
        })
        .on("mousemove", function() {
          $tooltipContainer.css("left", (d3.event.pageX + 20) + "px")
            .css("top", d3.event.pageY + "px");
        })
        .on("mouseout", function() {
          $tooltipContainer.html('');
        })
        .merge(bars)
        .attr("fill", function(item) {
          return color(item.answer_id);
        })
        .transition()
        // Set a transition for bars so that we have a slick update.
        .attr("x", function(item) { return x(item.answer_id); })
        .attr("width", x.bandwidth())
        .attr("y", function(item) { return y(item.count); })
        .attr("height", function(item) {
          return innerHeight - y(item.count);
        })

      // If the responses disappear from the API such that there is no information for the bar
      // (probably shouldn't happen),
      // remove the corresponding rect element.
      bars.exit().remove();

      // Update the X axis
      chart.select(".xaxis")
        .transition()
        .call(
          d3.axisBottom(x).tickFormat(function() {
            // Return null to output no text by default
            // The wrapText(...) call below will add text manually to let us adjust the angle and fit boundaries
            return null;
          })
        )
        .attr("transform", "translate(0," +
          (calcChartHeight() - ChartSettings.bottom - ChartSettings.top) +
        ")")
        .selectAll(".tick")
        .each(function(answerId, i, nodes) {
          var response = histogramLookup[answerId];
          var answerText = response ? response.answer_text : "";
          wrapText(d3.select(nodes[i]), x.bandwidth(), answerText);
        });

      // Update the Y axis.
      // By default it assumes a continuous scale, but we just want to show integers so we need to create the ticks
      // manually.
      var yTickValues = makeIntegerTicks(yDomainMax);
      chart.select(".yaxis")
        .transition() // transition to match the bar update
        .call(
          d3.axisLeft(y)
            .tickValues(yTickValues)
            .tickFormat(d3.format("d"))
            // make the tick stretch to cover the entire chart
            .tickSize(-innerWidth)
        )
        .selectAll(".tick")
        // render the tick line as a dashed line
        .attr("stroke-dasharray", function(value) {
          // At 0 the line should be solid to blend in with the chart border
          return value === 0 ? null : "8,8";
        })
        .selectAll("line")
        .attr("stroke", "rgba(0,0,0,.3)");

      // strangely, the default path has a line at the side and one at the top
      // we just want the one on the side
      chart.select(".yaxis .domain").remove();
      // Render a vertical line at x=0
      chart.select(".yaxis")
        .append("line")
        .classed("line", true)
        .attr("stroke", "#000")
        .attr("x2", 0.5);
      chart.select(".yaxis .line")
        .transition()
        .attr("y2", innerHeight);

      renderMathHJax(chart);
    }

    /**
     * Render buttons and select element above the chart
     */
    function renderControls() {
      var $buttonsRow = $element.find(buttonsRowSel);
      var $problemButton = $element.find(problemStatusBtnSel);
      var $problemStatusSpinner = $element.find(problemStatusSpinnerSel);
      var $problemStatusText = $element.find(problemStatusTextSel);
      var $problemWarning = $element.find(problemStatusWarningSel);
      var $timer = $element.find(timerSel);
      var $numStudents = $element.find(numStudentsSel);

      if (state.selectedRuns.length !== 1) {
        // Don't show these buttons for the the compare view
        $buttonsRow.toggleClass('hidden', true);
        return;
      }
      $buttonsRow.toggleClass('hidden', false);

      // using chartIndex 0 since this message should only get shown if there is only one chart visible
      if (state.runs.length > 0 && state.is_open) {
        var numStudentsMessage = makeNumStudentsMessage(getSelectedRun(0));
        $numStudents.text(numStudentsMessage);
      }
      $numStudents.toggleClass('hidden', !state.is_open);

      var uiState = UiStates[state.ui];
      $problemButton
        .text(uiState.buttonText || "")
        .toggleClass('hidden', uiState.buttonVis === false)
        .toggleClass('disabled', uiState.buttonEnabled === false);
      $problemStatusSpinner.toggleClass('hidden', uiState.spinnerVis === false);
      $problemStatusText
        .text(uiState.problemStatusText || "")
        .toggleClass('hidden', _.isUndefined(uiState.problemStatusText));
      $problemWarning
        .toggleClass('hidden', _.isUndefined(uiState.problemWarningText))
        .find(problemStatusWarningTextSel)
        .text(uiState.problemWarningText || "");
      $timer
        .toggleClass('hidden', !uiState.timerVis || false)
        .toggleClass('on', uiState.timerEnabled !== false);
    }

    /**
     * Returns a function that handles an error from an HTTP request promise.
     * @param {string} timeoutUiState The string representing the UI state to use if the error is a timeout.
     * @returns {Function} Error handling function
     */
    function generateErrorHandler(timeoutUiState) {
      return function(errorTextStatus) {
        // Don't do anything if the error text is 'abort' - that indicates that the request
        // was intentionally aborted.
        if (errorTextStatus === "abort") {
          return;
        }
        if (errorTextStatus === "timeout") {
          state.ui = timeoutUiState;
        } else {
          state.ui = "unknownError";
          if (state.responsesPollingTimeout) {
            clearTimeout(state.responsesPollingTimeout);
          }
        }
        renderControls();
      }
    }

    /**
     * Poll responses API. If the problem is open, schedule another poll using this function.
     */
    function pollForResponses() {
      if (state.is_open) {
        state.responsesPollingTimeout = setTimeout(pollForResponses, POLLING_MILLIS);
      }
      if (state.responsesAbortableRequest.isPending()) {
        state.responsesRequestAttemptCount += 1;
        if (state.responsesRequestAttemptCount === RESPONSES_ATTEMPTS_BEFORE_WARN) {
          state.ui = "openDelayed";
          renderControls();
        }
        return;
      }
      fetchResponsesAndRender();
    }

    /**
     * Read from the responses API and put the new value in the rendering state.
     */
    function fetchResponsesAndRender() {
      state.responsesAbortableRequest = makeAbortableRequest(responsesUrl);
      state.responsesAbortableRequest.promise.then(function(newState) {
        _.assign(state, newState, {
          lastFetch: moment()
        });
        state.ui = state.is_open ? "open" : "closed";
        renderAll();
      }).fail(
        generateErrorHandler("openTimedOut")
      ).always(function () {
        state.responsesRequestAttemptCount = 0;
      });
    }

    function startTimer(startingMsElapsed) {
      var start;
      var startingSeconds = startingMsElapsed ? Math.floor(startingMsElapsed / 1000) : 0;
      var $timer = $element.find(timerSel);
      $timer.text(formatTime(startingSeconds));
      start = Date.now();
      startingMsElapsed = startingMsElapsed || 0;
      state.timerInterval = setInterval(function() {
        var deltaMillis = Date.now() - start + startingMsElapsed; // milliseconds elapsed since start
        var deltaSeconds = Math.floor(deltaMillis / 1000);
        $timer.text(formatTime(deltaSeconds));
      }, TIMER_MILLIS);
    }

    function stopTimerCount() {
      if (state.timerInterval) {
        clearInterval(state.timerInterval);
      }
    }

    function resetTimer() {
      var $timer = $element.find(timerSel);
      stopTimerCount();
      $timer.text(formatTime(0));
    }

    function handleProblemStatusClick(e) {
      if (state.is_open) {
        state.ui = "closing";
        stopTimerCount();
      } else {
        state.ui = "opening";
      }
      renderAll();

      // Clear any in-progress request to the responses endpoint, and the timeout for polling that endpoint.
      if (state.responsesPollingTimeout) {
        clearTimeout(state.responsesPollingTimeout);
      }
      if (state.responsesAbortableRequest.isPending()) {
        state.responsesAbortableRequest.abort();
      }

      // Make the request to toggle the problem status
      var changeStatusAbortableRequest = makeAbortableRequest(toggleStatusUrl);
      changeStatusAbortableRequest.promise.then(function(newState) {
        // Selected runs should be reset when the open status is changed
        _.assign(state, newState, {
          selectedRuns: [null]
        });

        if (state.is_open) {
          state.ui = "open";
          renderAll();
          startTimer();
          pollForResponses();
        }
      });

      // If the problem is successfully toggled and the problem was closed, make one final request
      // to the responses endpoint for updated responses to the problem.
      var finalResponsesRequestPromise = changeStatusAbortableRequest.promise.then(function () {
        if (state.is_open) { return true; }
        state.ui = "fetchingFinal";
        renderControls();
        state.responsesAbortableRequest = makeAbortableRequest(responsesUrl);
        return state.responsesAbortableRequest.promise;
      });

      // Show a failure message if either the problem status toggle request fails or the final
      // request for problem responses fails.
      finalResponsesRequestPromise.then(function (newState) {
        if (!state.is_open) {
          _.assign(state, newState, {
            lastFetch: moment()
          });
          state.ui = "closed";
          resetTimer();
          renderAll();
        }
      }).fail(generateErrorHandler("closingTimedOut"));
    }

    $(function($) { // onLoad
      // Configure MathJax settings for any problems with math notation
      MathJax.Hub.Config({
        tex2jax: {
          inlineMath: [ ['$','$'], ["\\(","\\)"] ],
          processEscapes: true
        }
      });

      // There can be only one tooltip container
      $tooltipContainer = $(tooltipContainerSel);
      if ($tooltipContainer.length === 0) {
        $tooltipContainer = $(
          '<div class="rapid-response-tooltip-container"></div>'
        );
        $("body").prepend($tooltipContainer);
      }

      $element.find(problemStatusBtnSel).click(handleProblemStatusClick);

      // This contains the staff-only buttons
      var $wrapInstructorInfo = $element.parent().find(".wrap-instructor-info");
      if ($wrapInstructorInfo.length) {
        // The BR element will clear the floats via the CSS rule
        $wrapInstructorInfo.append($("<br />"));
      }

      // Render an initial view before making the first request for problem responses & current state
      state.ui = "initial";
      renderControls();

      state.responsesAbortableRequest = makeAbortableRequest(
        responsesUrl,
        {timeout: INIT_REQUEST_TIMEOUT_MILLIS}
      );
      state.responsesAbortableRequest.promise.then(function(newState) {
        _.assign(state, newState, {
          lastFetch: moment(),
          responsesRequestAttemptCount: 0
        });

        state.ui = state.is_open ? "open" : "closed";
        renderAll();
        if (state.is_open) {
          var millisSinceOpen = state.runs && state.runs.length === 0
            ? 0
            : moment().diff(state.lastFetch) + moment(state.server_now).diff(moment(state.runs[0].created));
          startTimer(millisSinceOpen);
          pollForResponses();
        } else {
          resetTimer()
        }
      }).fail(generateErrorHandler("loadingTimedOut"));

      // adjust graph for each rerender
      window.addEventListener('resize', function() {
        renderAll();
      });
    });
  }

  function initializeRapidResponseAside(runtime, element) {
    return new RapidResponseAsideView(runtime, element);
  }

  window.RapidResponseAsideInit = initializeRapidResponseAside;
}($, _, MathJax));
