text: let controlsDiv, resultsDiv
  let controls = {}
  
  let groups = 0
  let ofSize = 0
  let forRounds = 0
  let playerNames = []
  let forbiddenPairs = Immutable.Set()
  
  let startTime;
  let lastResults
  const myWorker = new Worker('lib/worker.js');
  
  function init() {
    myWorker.addEventListener('message', onResults, false);
  
    controlsDiv = document.getElementById('controls')
    resultsDiv = document.getElementById('results')
  
    controls.recomputeButton = controlsDiv.querySelector('#recomputeButton')
    controls.groupsLabel = controlsDiv.querySelector('#groupsLabel')
    controls.groupsSlider = controlsDiv.querySelector('#groupsSlider')
    controls.ofSizeLabel = controlsDiv.querySelector('#ofSizeLabel')
    controls.ofSizeSlider = controlsDiv.querySelector('#ofSizeSlider')
    controls.forRoundsLabel = controlsDiv.querySelector('#forRoundsLabel')
    controls.forRoundsSlider = controlsDiv.querySelector('#forRoundsSlider')
    controls.playerNames = controlsDiv.querySelector('#playerNames')
    controls.forbiddenPairs = controlsDiv.querySelector('#forbiddenPairs')
  
    // User input controls
    controls.recomputeButton.onclick = recomputeResults;
    controls.groupsSlider.oninput = onSliderMoved
    controls.ofSizeSlider.oninput = onSliderMoved
    controls.forRoundsSlider.oninput = onSliderMoved
    controls.playerNames.onkeyup = onPlayerNamesKeyUp
    controls.playerNames.onchange = onPlayerNamesChanged
    controls.forbiddenPairs.onchange = onForbiddenPairsChanged
  
    playerNames = readPlayerNames()
    forbiddenPairs = readForbiddenPairs(playerNames)
    onSliderMoved()
    recomputeResults()
  }
  
  function onResults(e) {
    lastResults = e.data
    renderResults()
    if (lastResults.done) {
      enableControls()
    }
  }
  
  function recomputeResults() {
    startTime = Date.now();
    lastResults = null;
    renderResults()
    disableControls()
    myWorker.postMessage({groups, ofSize, forRounds, forbiddenPairs: forbiddenPairs.toJS()})
  }
  
  function onSliderMoved() {
    groups = parseInt(controls.groupsSlider.value, 10)
    ofSize = parseInt(controls.ofSizeSlider.value, 10)
    forRounds = parseInt(controls.forRoundsSlider.value, 10)
  
    // Update labels
    controls.groupsLabel.textContent = groups
    controls.ofSizeLabel.textContent = ofSize
    controls.forRoundsLabel.textContent = forRounds
  }
  
  function disableControls() {
    controls.recomputeButton.disabled = true
    controls.groupsSlider.disabled = true
    controls.ofSizeSlider.disabled = true
    controls.forRoundsSlider.disabled = true
    controls.playerNames.disabled = true
    controls.forbiddenPairs.disabled = true
    
    // Show spinner
    controls.recomputeButton.innerHTML = '&nbsp;<span class="spinner"></span>'
  }
  
  function enableControls() {
    controls.recomputeButton.disabled = false
    controls.groupsSlider.disabled = false
    controls.ofSizeSlider.disabled = false
    controls.forRoundsSlider.disabled = false
    controls.playerNames.disabled = false
    controls.forbiddenPairs.disabled = false
    
    // Hide spinner
    controls.recomputeButton.innerHTML = 'Recompute!'
  }
  
  function readPlayerNames() {
    return controls.playerNames.value
      .split('\n')
      .map(name => name.trim())
  }
  
  function onPlayerNamesKeyUp() {
    playerNames = readPlayerNames()
    renderResults()
  }
  
  function onPlayerNamesChanged() {
    playerNames = readPlayerNames()
    renderResults()
  }
  
  function onForbiddenPairsChanged() {
    forbiddenPairs = readForbiddenPairs(playerNames)
  }
  
  /**
   * Given the current playerNames and the value of the forbiddenPairs input field,
   * recomputes the cached set of forbiddenPairs by index.
   * @param {Array<string>} playerNames
   * @return {Immutable.Set<Immutable.Set<number>>}
   */
  function readForbiddenPairs(playerNames) {
    return controls.forbiddenPairs.value
      .split('\n')
      .map(stringPair =>
        stringPair
          .split(',')
          .map(name =>name.trim())
      )
      .filter(pair => pair.length === 2) // Drop lines that aren't pairs
      .reduce((memo, [leftName, rightName]) => {
        const leftIndices = indicesOf(leftName, playerNames)
        const rightIndices = indicesOf(rightName, playerNames)
        for (const leftIndex of leftIndices) {
          for (const rightIndex of rightIndices) {
            if (leftIndex !== rightIndex) {
              memo = memo.add(Immutable.Set([leftIndex, rightIndex]))
            }
          }
        }
        return memo
      }, Immutable.Set())
  }
  
  function indicesOf(needle, haystack) {
    const indices = []
    let nextIndex = -1
    do {
      nextIndex = haystack.indexOf(needle, nextIndex + 1)
      if (nextIndex > -1) indices.push(nextIndex)
    } while (nextIndex > -1)
    return indices
  }
  
  function playerName(i) {
    return playerNames[i] ? playerNames[i] : `Player ${i+1}`
  }
  
  function downloadCsv() {
    // Pivot results into a table that's easier to work with
    const roundNames = lastResults.rounds.map((_, i) => `Round ${i + 1}`)
    // for any given round, the total number of players is the number of groups in the round, times the number of people in each round
    const playerCount = lastResults.rounds[0].length * lastResults.rounds[0][0].length
    
    // Stub out a row for each player
    const players = []
    for (let i = 0; i < playerCount; i++) {
      players.push([playerName(i)])
    }
    
    // Fill in assigned groups
    lastResults.rounds.forEach((round) => {
      round.forEach((group, j) => {
        group.forEach(playerIndex => {
          players[playerIndex].push(`Group ${j + 1}`)
        })
      })
    })
    
    // Build table
    const rows = [
      ['', ...roundNames],
      ...players
    ]
    // For debugging: console.table(rows);
    
    let csvContent = "data:text/csv;charset=utf-8," 
      + rows.map(e => e.join(",")).join("\n");
    
    const encodedUri = encodeURI(csvContent)
    const link = document.createElement("a")
    link.setAttribute("href", encodedUri)
    link.setAttribute("download", "golfer_solution.csv")
    document.body.appendChild(link)
    link.click()
  }
  
  function renderResults() {
    resultsDiv.innerHTML = ''
    if (lastResults) {
      lastResults.rounds.forEach((round, roundIndex) => {
        const roundDiv = document.createElement('div')
        roundDiv.classList.add('round')
    
        const header = document.createElement('h1')
        header.textContent = `Round ${roundIndex+1}`
        const conflictScore = document.createElement('div')
        conflictScore.classList.add('conflictScore')
        conflictScore.textContent = `Conflict score: ${lastResults.roundScores[roundIndex]}`
        header.appendChild(conflictScore)
    
        const groups = document.createElement('div')
        groups.classList.add('groups')
    
        round.forEach((group, groupIndex) => {
          const groupDiv = document.createElement('div')
          groupDiv.classList.add('group')
          const groupName = document.createElement('h2')
          groupName.textContent = `Group ${groupIndex + 1}`
          groupDiv.appendChild(groupName)
    
          const members = document.createElement('ul')
          group.sort((a, b) => parseInt(a) < parseInt(b) ? -1 : 1).forEach(personNumber => {
            const member = document.createElement('li')
            member.textContent = playerName(personNumber)
            members.appendChild(member)
          })
          groupDiv.appendChild(members)
    
          groups.appendChild(groupDiv)
        })
    
        roundDiv.appendChild(header)
        roundDiv.appendChild(groups)
        resultsDiv.appendChild(roundDiv)
      })
      
      if (lastResults.done) {
        // Summary div - total time and CSV download
        const summaryDiv = document.createElement('div')
        summaryDiv.style.borderTop = 'solid #aaaaaa thin'
        summaryDiv.style.padding = '7px 0'
        const csvButton = document.createElement('button')
        csvButton.type = 'button'
        csvButton.appendChild(document.createTextNode('Download CSV'))
        csvButton.onclick = downloadCsv
        const elapsedSecs = Math.round((Date.now() - startTime) / 100) / 10
        const elapsedTime = document.createElement('span')
        elapsedTime.style.margin = '0 1em'
        elapsedTime.style.fontStyle = 'italic'
        elapsedTime.style.fontSize = 'smaller'
        elapsedTime.textContent = `Computed in ${elapsedSecs} seconds.`
        summaryDiv.appendChild(csvButton)
        summaryDiv.appendChild(elapsedTime)
        resultsDiv.appendChild(summaryDiv)
      } else {
        resultsDiv.appendChild(document.createTextNode('Thinking...'));
      }
    }
  }
  
  document.addEventListener('DOMContentLoaded', init)
  